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.