Introduction
This article is a follow-up article on my real-time charting series with HTML5 and Spike-Engine. This article serves as a demonstration of HTML5 facilities and Smoothie Charts in order to render a dynamic chart, streamed in real-time from an application server:
- It uses websockets internally, but abstracted by Spike-Engine, will fallback to flash sockets for older browsers.
- It updates the gauges using a publish-subscribe model with JSON formatted packets.
- It uses Smoothie Charts library for rendering the chart, which are rendered in Canvas2D for optimal performance.
- It is cross-platform and with a minimized packet payload and message compression.
- The application server is a self-hosted executable and the client is just a plain HTML file.
Background
As a follow-up on my real-time Gauge article, I wanted to create a streamed chart that would show the same information (packets per second rate), in a form of a streamed line chart. This is a handy approach as it allows not only to see one value at a time, but a whole pattern.
Making the Server
To collect the data, we use a rolling window method, as in my previous article. This time, however, I created a nicer encapsulation of the algorithm, creating a RollingQueue
which maintains its size and automatically shifts the sampling window. The code is fairly straightforward:
public class RollingQueue<T> : Queue<T>
{
public RollingQueue(int size)
{
this.Size = size;
}
public int Size
{
get;
set;
}
new public void Enqueue(T item)
{
base.Enqueue(item);
if (this.Count > this.Size)
this.Dequeue();
}
}
First, we create and make the service listen on any IPAddress
available:
Service.Listen(new TcpBinding(IPAddress.Any, 8002));
Next, we create a PubHub
instance which acts as a publish-subscribe channel. This implements publish-subscribe model. In publish-subscribe model senders of messages, called publishers, do not program the messages to be sent directly to specific receivers, called subscribers. Instead, published messages are characterized into classes, without knowledge of what, if any, subscribers there may be. Similarly, subscribers express interest in one or more classes, and only receive messages that are of interest, without knowledge of what, if any, publishers [Wiki].
In addition, we also need to hook up ClientSubscribe
event which would allow us to send the history to the new subscriber. This is needed as we want to populate the newly created chart once a client connects, in that way we show a full chart from the beginning.
Then we simply a schedule function to be called every 200 milliseconds, this function will publish messages to the PubHub
.
[InvokeAt(InvokeAtType.Initialize)]
public static void Initialize()
{
var hub = Spike.Service.Hubs.GetOrCreatePubHub("PacketWatch");
hub.ClientSubscribe += OnClientSubscribe;
hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);
}
To make everything work, we need to maintain two rolling windows:
- First window calculates the packet per second rate. Since we sample at the 200 millisecond rate, we need a window of 5 (200ms * 5 = 1 second) to calculate the rate.
- Second window will contain the statistics for the past 30 seconds. Again, since we sample every 200 milliseconds, we need a window of 150 to make it work.
private static RollingQueue<long> Sampler = new RollingQueue<long>(5);
private static RollingQueue<long> History = new RollingQueue<long>(150);
After that, the implementation is quite straightforward and similar to the Gauge article I wrote before:
private static void OnTick(IHub hub)
{
var pubHub = hub as PubHub;
var packetsIn = Monitoring.PacketsIncoming.Value;
var packetsOut = Monitoring.PacketsOutgoing.Value;
var packetsDelta = (packetsIn + packetsOut) - PacketCount;
PacketCount = packetsIn + packetsOut;
Sampler.Enqueue(packetsDelta);
var pps = Sampler.Sum();
History.Enqueue(pps);
pubHub.Publish(pps);
}
private static void OnClientSubscribe(IHub sender, IClient client)
{
var pubHub = sender as PubHub;
pubHub.Publish(
client,
null,
History.ToArray()
);
}
Making the Client
Now, let's examine how the client is made. I used the awesome Smoothie Charting library by Joe Walnes. It renders the chart in Canvas2D
element, making it really smooth. Canvas2D
element is hardware-accelerated in most modern browsers, so you can expect a good framerate, even on mobile devices.
This charting library is very easy to use, and they even provide a nice online builder allowing you to generate the code and tweak the look and feel of the chart.
We start by adding few dependancies to our HTML page.
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="spike-sdk.min.js"></script>
<script src="http://smoothiecharts.org/smoothie.js"></script>
Next, we add the Canvas2D
element to the HTML DOM which would contain the chart itself. I defined a width of 600 pixels, and we are going to have 1 pixel representing 50 milliseconds of data. Since we sample every 200 milliseconds, that means that we will have a data point every 4 pixels, ending up at 150 datapoints rendererd at once in our chart. In other words, our chart will be able to hold 30 seconds of data.
<canvas id="smoothie-chart" width="600" height="125"></canvas>
Next step is, in javascript, to create our chart and a TimeSeries that represent a time series for our data. Notice the millisPerPixel
value being set to 50, as mentioned above.
var chart = new SmoothieChart({
millisPerPixel: 50,
grid: {
fillStyle: 'transparent',
strokeStyle: 'rgba(166,197,103,0.20)',
sharpLines: true,
millisPerLine: 4000,
verticalSections: 8,
borderVisible: false
},
labels: { fillStyle: '#000000' }
}),
canvas = document.getElementById('smoothie-chart'),
series = new TimeSeries();
chart.addTimeSeries(series, { lineWidth: 2, strokeStyle: '#A6C567', fillStyle: 'rgba(166,197,103,0.20)' });
chart.streamTo(canvas, 500);
Next, we connect to our remote server using ServerChannel from Spike-Engine library. Once connected, it subscribes to our PubHub and is able to receive messages.
var server = new spike.ServerChannel("127.0.0.1:8002");
server.on('connect', function () {
server.hubSubscribe('PacketWatch', null);
});
Finally, we need to hook hubEventInform
event that will handle the messages received from the server. In this case, we have two cases:
- First case, if the value we got is an array. This means we've received a history and we need to populate the chart. We populat the chart by appending to the
TimeSeries
. However, we also need to provide the time of the data point. To do it, we calculate the time backwards. To do so:
- We get the current time in milliseconds
- We subtract 30000 milliseconds from it (30 seconds)
- For each point, we calculate the time of the occurence
- The second case is when the message we received is just a value, and this is handled by simply appending to the
TimeSeries
.
server.on('hubEventInform', function (p) {
var value = JSON.parse(p.message);
if ($.isArray(value)) {
var time = new Date().getTime();
var length = value.length;
var element = null;
for (var i = 0; i < length; i++) {
element = value[i];
series.append(time - 30000 + (i * 200), element);
}
} else {
var count = $('#packetCounter');
count.text(value);
series.append(new Date().getTime(), value);
}
});
History
- 23/06/2015 - Source code & article updated to Spike v3
- 09/09/2013 - Initial release.