Websockets with Angular 2.0 RC1 – Graphing a stream of data

by
Tags: , , , ,
Category:

Plot data in real time in Angular 2 RC1


This article focuses on how to plot data in real(ish) time with WebSockets and Angular 2 RC1. The sample application at angular2-websocket-plotter shows how to generate websocket data in NodeJS, subscribe to it in an Angular2 service, and plot it from a component using a project called Smoothie Chart.

WebSockets are for Streaming Data!

A WebSocket is a persistent connection to a server which receives payloads of information as they are made available by the server. WebSockets are bi-directional – they can both receive data and send data back to the server. The data can take any form needed. In our case, we're using JSON for our data format, as it is easy to parse in Angular and easy to generate in a Node application.

There is one caveat to using WebSockets and any other data streaming or fetching API – they must reside either on the same server and port, or the server must communicate rights to the client via the CORS protocol to allow the usage of the resource from outside of the server. We're going to bypass CORS for this demo and provide both the server and proxy to an Angular web server from an Express app.

Following Along

You can clone the repo and install it, following the README. I won't show things like import statements or all details here, just enough to give you an idea of how the solution was written. So pull down the source or browse it in another tab while reading for reference.

Of particular note – when moving from the betas to RC1, the Angular team re-packaged everything into smaller npm modules. So you have @angular/core in npm now as well as common, http, and others. It is actually a good move, making the project more truly modular, but it put a snag in your world if you were following along through the beta process.

FORTUNATELY the wonderful angular-cli team kept us up to date, and at the conference released 0.1.0 of the tool which WORKS with RC1. This project uses that tool so be sure to follow my README.md file to the letter.

Creating a Node WebServer with WebSockets

I've installed express, express-http-proxy and websockets to my package.json file, which gives me enough to stream a sine wave of data down the line to a client. I've provided a proxy to angular-cli's runtime engine (which you get when running ng serve) so that hitting localhost:3000 puts you into the root of the webapp, proxied on port 4200. Even better, the proxy gets ignored when you request the websocket, which sits on the root.

We'll start out with the setup of the Express and WebSocket server:

(function() {
    'use strict';
    var express = require('express');
    var proxy = require('express-http-proxy');
    var http = require('http');
    var WebSocketServer = require('websocket').server;
    const app = express();
    const server = http.createServer(app);

At this point, we have hooks to both a Node Express application for routing our normal URLs, and a WebSocketServer constructor function to configure our websocket server engine. We've also installed express-http-proxy which lets us proxy any URL we want to another server engine.

Now let's set up a route in Express to proxy all calls for content over to port 4200, which is where the Angular Client is hosting the application:

    app.use('/', proxy('localhost:4200', {
         forwardPath: function(req, res) {
            return require('url').parse(req.url).path;
    }}));

Next, we'll set up the WebSocket server, listening on all requests for a WebSocket protocol. We'll have it accept only clients looking for a sineserver service:

    const wsServer = new WebSocketServer({
        httpServer: server,
        autoAcceptConnections: false
    });
    function originIsAllowed(origin) {
        // TODO SOMEDAY
        return true;
    }
    wsServer.on('request', function(request) {
        if (!originIsAllowed(request.origin)) {
            request.reject();
            console.log((new Date()) +
              ' Connection fron origin ' +
              request.origin + ' rejected.');
            return;
        }
        var connection = request.accept('sinedata', request.origin);
        observableSineWave(this, .1, 10);
        connection.on('message', function(message) {
            connection.sendUTF(message.utf8Data);
        });
        connection.on('close', function(reasonCode, description) {
            console.log((new Date()) + ' Peer ' +
               connection.remoteAddress +
               ' disconnection. Reason: ' + reasonCode);
        });
    });

Notice the call to observableSineWave – this begins generating websocket client messages, broadcasting them to all clients listening to the server.

Here's the function:

    function observableSineWave(serverSocket, increment, period) {
        let waveVal = 0;
        setInterval(function() {
            waveVal = waveVal == period ? 0 : waveVal + increment;
            serverSocket.broadcast(
              JSON.stringify({ value: Math.sin(waveVal) }));
        }, period);
    }

Finally, let's boot the web server:

    server.listen(3000, function () {
        console.log('Example app listening on port 3000!');
    });
// and we're done!
}());

(I'll break that down better in the future, but it's secondary to understanding the Angular code itself).

TL;DR – the server creates a WebSocket on /, and also hosts the client application by proxying (server-side) from port 3000 to port 4200 which will be what our angular-cli project serves.

Now we can get started subscribing to the WebSocket and plotting our data.

Connecting to a WebSocket in JavaScript

To connect a WebSocket we just use the native WebSocket constructor, and we can use window to provide us the current host and port programmatically. Our src/app/sinewave-data.service.ts file does this:

  let ws: WebSocket = new WebSocket('ws://' +
            window.location.hostname + ':' +
            window.location.port + '/deviceData');

Now, to get this to play nice with Angular, we have to provide a streaming object to report our updates from the WebSocket. I am using a ReplaySubject from RxJs for this purpose. It lets me provide my own data stream, and send it to the subject when data arrives.

So the path is:

server ws emit --->
    client ws onMessage -->
       replaysubject -> (buffer n values)
          --> subscription (UI)

Here is the implementation code in our service:

export class SineWaveDataService {
  observableSineWave(increment: number, period: number) : ReplaySubject<string> {
      let subject = new ReplaySubject<string>(100);
      let ws = new WebSocket('ws://' + window.location.hostname +
                             ':' + window.location.port, 'sinedata');
      ws.onmessage = function(e: MessageEvent) {
          return subject.next(e.data)
      };
      // TODO - error handling, restart logic, etc.
      // in a future blog post.
      return subject;
  }
}

Visualizing Data with Angular 2 and Smoothie

So far we've just discussed how to get data to a subscriber, but not how to display that data. Let's integrate a graphing library. You can use any one you want, but we'll choose SmoothieCharts.

To install it using the angular-cli project, do this from your project root:

npm install --save smoothie

Then you have to add to your typings. Make sure you've installed typings with npm install -g typings and do this:

typings install --save --ambient smooothie

Next, you have to tell the packaging process in the angular-cli to add smoothie to the list of files that carry over. Add smoothie/smoothie.js to the bottom of your vendorNpmFiles in ./angular-cli-build.js. It should now look something like this:

/* global require, module */
var Angular2App = require('angular-cli/lib/broccoli/angular2-app');
module.exports = function(defaults) {
  return new Angular2App(defaults, {
    vendorNpmFiles: [
      'systemjs/dist/system-polyfills.js',
      'systemjs/dist/system.src.js',
      'zone.js/dist/*.js',
      'es6-shim/es6-shim.js',
      'reflect-metadata/*.js',
      'rxjs/**/*.js',
      '@angular/**/*.js',
      'smoothie/smoothie.js'
    ]
  });
};

Finally to get SystemJS to load the smoothie library as a script file, we edit the map property of System.config in src/system-config.ts and add our script file:

...
System.config({
  map: {
    '@angular': 'vendor/@angular',
    'rxjs': 'vendor/rxjs',
    'main': 'main.js',
    'smoothie': 'vendor/smoothie/smoothie.js'  <-- new
  },
  packages: cliSystemConfigPackages
});
...

Follow this technique to integrate any library into Angular2 with the angular-cli build.

The component

Now you have the smoothie library integrated into your loader, your source packager, and your typings. You can start building it into a component. We'll create the plotter component. It will have a template that includes a canvas, which SmoothieChart uses to render its content.

We aren't doing this perfectly; I'll update this periodically as I find better ways to embed these libraries. For example, I'm cheating and using a few any types here.

Also, I'm using ElementRef and NativeElement, which are a no-no. Expect this to be updated with the One True Path soon.

A simple plotter

We'll start by creating a PlotterComponent which takes a width, height, and incomingData$ stream.

Here is the basic shell of the component:

import {Component, Input, ElementRef, OnInit, OnChanges,
        ChangeDetectorRef, ChangeDetectionStrategy, NgZone} from '@angular/core';
import {ReplaySubject} from 'rxjs/Rx';
import {SmoothieChart} from "smoothie";
import {TimeSeries} from "smoothie";
@Component({
  selector: 'plotter',
  inputs: [
    'dataSet', 'width', 'height'
  ],
  template: `
    <h3>Plot</h3>
    <canvas id="{{id}}" width="{{width}}" height="{{height}}"></canvas>
  `
})
export class Plotter implements OnInit, OnChanges {
  @Input() id: string;
  @Input() width: number;
  @Input() height: number;
  @Input() incomingData$: Array<string>;
  chart: any;
  sineLine: any;
  constructor(private element: ElementRef, private ngZone: NgZone) {
  }
}

So now we have a basic shell for our app. Let's install Smoothie on the canvas in our ngInit() function:

  ngOnInit() {
    this.chart = new SmoothieChart();
    this.sineLine = new TimeSeries();
    this.chart.addTimeSeries(this.sineLine);
    this.chart.streamTo(
        this.element.nativeElement.getElementsByTagName('canvas')[0]);
  }

During initialization, we create a SmoothieChart, add it to the first canvas of our element, and attach a time series to it.

For our change detection, first we'll modify our core component, setting our template to something like this:

  template: `
    <plotter
         width="900"
         height="100"
         [incomingData$]="incomingData$ | async"></plotter>
  `, ...

Note the pipe, async – this really is an amazing feature. It sends along the latest subscribed payload point to the input of the plotter component automatically. In fact, you don't even need to subscribe to the observable in the parent OR the child component.

In our Plotter child, we accept our changes in ngChanges, which sends a changes object containing the various components that change.

In our case, we only care about changes to the incomingData$ observed component payload. So we first check for a current value, then that the value has a property from the websocket payload called value:

   ngOnChanges(data) {
       if (data.incomingData$) {
           if (data.incomingData$.currentValue) {
               this.ngZone.runOutsideAngular(() => {
                 this.sineLine.append(new Date().getTime(),
                    JSON.parse(data.incomingData$.currentValue).value);
               });
           }
       }
   }

I've also included NgZone so that you can see how we update a component outside of Angular itself. The Smoothie Chart library doesn't have anything to do with Angular, it is a sink of data for display purposes and does nothing else. So we tell it to schedule itself outside of Angular.

Wrapping up

So that's it. Getting started with Angular2 can be daunting, but you should start by getting ahold of the angular-cli project, installing it globally, and experimenting. I particularly like running inside of a node proxy so that we can get access to the server API – feel free to play with this sample and extend it to provide your REST APIs as well.

Resources


Looking for private training?

We offer training in a number of key technologies like Angular, Angular 2, Scala, and Akka. We’ll come to you, or you can learn at our training facility near Philadelphia, PA. Visit our training page for more.

You can contact us for a quote on private training, group or individual mentoring.

Contact