Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

HTML5 Streamed Chart with Smoothie Charts and Spike-Engine

4.96/5 (15 votes)
23 Jun 2015CPOL4 min read 52.8K   913  
Streamed chart implementation using modern HTML5 features.

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:

  1. It uses websockets internally, but abstracted by Spike-Engine, will fallback to flash sockets for older browsers.
  2. It updates the gauges using a publish-subscribe model with JSON formatted packets.
  3. It uses Smoothie Charts library for rendering the chart, which are rendered in Canvas2D for optimal performance.
  4. It is cross-platform and with a minimized packet payload and message compression.
  5. The application server is a self-hosted executable and the client is just a plain HTML file.

Image 1

[View a live demo]

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:

C#
/// <summary>
/// Represents a rolling queue.
/// </summary>
public class RollingQueue<T> : Queue<T>
{
    /// <summary>
    /// Constructs a new rolling queue.
    /// </summary>
    /// <param name="size">The size of the queue to maintain.</param>
    public RollingQueue(int size)
    {
        this.Size = size;
    }
 
    /// <summary>
    /// Gets or sets the size of the rolling queue.
    /// </summary>
    public int Size
    {
        get;
        set;
    }
 
    /// <summary>
    /// Enqueues an item to the rolling queue.
    /// </summary>
    /// <param name="item">The item to enqueue.</param>
    new public void Enqueue(T item)
    {
        // Enqueue a new item
        base.Enqueue(item);
 
        // Dequeue if there's too many elements
        if (this.Count > this.Size)
            this.Dequeue();
    }
 
}

First, we create and make the service listen on any IPAddress available:

C#
// Start listening on the port 8002
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.

C#
/// <summary>
/// This function will be automatically invoked when the service starts
/// listening and accepting clients.
/// </summary>
[InvokeAt(InvokeAtType.Initialize)]
public static void Initialize()
{
    // We create a PubHub which acts as publish-subscribe channel. This allows us to publish 
    // simple string messages and remote clients can subscribe to the publish notifications.
    var hub = Spike.Service.Hubs.GetOrCreatePubHub("PacketWatch");
 
    // Hook up the event so we know when a new client have subscribed
    hub.ClientSubscribe += OnClientSubscribe;
 
    // We schedule the OnTick() function to be executed every 200 milliseconds. 
    hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);
}  

To make everything work, we need to maintain two rolling windows:

  1. 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.
  2. 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.
//// <summary>
// A queue to hold our packets. We need this to calculate a floating sum.
/// </summary>
private static RollingQueue<long> Sampler = new RollingQueue<long>(5);
 
/// <summary>
/// A queue to hold the history.
/// </summary>
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:

/// <summary>
/// Occurs when our timer ticks.
/// </summary>
private static void OnTick(IHub hub)
{
    // Cast is as PubHub
    var pubHub = hub as PubHub;
 
    // In this calse, we're just taking those values from Spike-Engine itself, but
    // you could replace it to get values from elsewhere.
    var packetsIn = Monitoring.PacketsIncoming.Value;
    var packetsOut = Monitoring.PacketsOutgoing.Value;
 
    // Compute the delta
    var packetsDelta = (packetsIn + packetsOut) - PacketCount;
    PacketCount = packetsIn + packetsOut;
 
    // Maintain a queue of 5 elements, to match for a second (200 ms * 5 = 1 second)
    Sampler.Enqueue(packetsDelta);
 
    // Calculate the sum
    var pps = Sampler.Sum();
 
    // Add to the history
    History.Enqueue(pps);
 
    // Publish the packet rate
    pubHub.Publish(pps);
}
 
/// <summary>
/// Occurs when a client subscribes to a hub.
/// </summary>
private static void OnClientSubscribe(IHub sender, IClient client)
{
    // Cast is as PubHub
    var pubHub = sender as PubHub;
 
    // Publish the history only to our newly subscribed client (not the others).
    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.

Image 2

We start by adding few dependancies to our HTML page.

HTML
<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.

HTML
<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.

JavaScript
// Create the chart
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.

JavaScript
var server = new spike.ServerChannel("127.0.0.1:8002");

// When the browser is connected to the server
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:

  1. 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
  2. The second case is when the message we received is just a value, and this is handled by simply appending to the TimeSeries.
JavaScript
// When we got a notification from the server
server.on('hubEventInform', function (p) {
    var value = JSON.parse(p.message);
    if ($.isArray(value)) {
        // If this is an array, that means we got the history
        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 {
        // Update the counter
        var count = $('#packetCounter');
        count.text(value);

        // Append a point
        series.append(new Date().getTime(), value);
    }
});

History

  • 23/06/2015 - Source code & article updated to Spike v3
  • 09/09/2013 - Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)