Click here to Skip to main content
Click here to Skip to main content

Real-Time Gauge with ChartJS and Spike-Engine

, 8 Sep 2013
Rate this:
Please Sign up or sign in to vote.
Presents a real-time gauge implementation that can be used for building dashboards

Introduction

Everything in the web becomes increasingly real-time and HTML5 finally offers some facilities to build efficient, simple and robust real-time applications on the web. This article serves as a demonstration of such facilities in building a gauge, updated in real time 5 times per second which you can use for server monitoring or building dashboards:

  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 ChartJS library for rendering the gauges, the gauges are rendered as SVG.
  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.

[View a live demo]

Background

While browsing around the internet, I stumbled upon a very nice HTML5 charting library by DevExpress, called ChartJs. They provide a nice declarative API for creating various good looking charts that can be potentially used for visual analytics and dashboards. However, none of their examples seemed to show how to update their charts in a real-time client-server way.

Hence, I decided to create a simple gauge that would monitor packets per second rate on my server, using Spike-Engine as a backend.

Making the Server

Let's look at our server implementation, our server needs to perform several things in order to update the gauges:

  1. Listen on a particular endpoint (IP + port) and create a publish-subscribe channel for posting our messages to the client.
  2. Compute packets per second, and publish the value to all subscribed clients 5 times per second (every 200 milliseconds).

The first point is rather easy with Spike-Engine, we need to simply call Service.Listen method in order to start listening on a specific IPAddress and a port (called endpoint), or on several endpoints for that matter. This line in the Main method accomplishes it:

Service.Listen(new TcpBinding(IPAddress.Any, 8002));

Next, we create a new PubHub instance, provided us by Spike-Engine. 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 there are [Wiki].

We first create our PubHub instance and give it a name. The name is important as we will need to provide the same name in our clients when we want to subscribe.

var hub = Spike.Service.Hubs.GetOrCreatePubHub("PacketWatch");  

Then we schedule a function to be called every 200 milliseconds, this function will publish messages to the PubHub.

hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);

That's it for the point 1. By now, we have a publish subscribe channel (PubHub) and scheduled execution of a function named OnTick.

Now, let's look at point 2 as we need to compute packets per second rate. We do this by implementing a simple rolling window periodic sampling and we sample every 200 milliseconds.

This can be done fairly easily by maintaining a Queue of elements and sampling the delta value (difference) to it. We need a queue as the window needs to be shifted, this is accomplished using a Dequeue() call when there's more than 5 elements in the queue. It allows us to have a precise rate count, with a 200 millisecond resolution. Here's the implementation of our OnTick method, called 5 times per second.

    private static void OnTick(IHub hub)
    {
        // Cast is as PubHub
        var pubHub = hub as PubHub;
 
        // In this case, 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)
        PacketSampler.Enqueue(packetsDelta);
        if (PacketSampler.Count > 5)
            PacketSampler.Dequeue();
 
        // Publish the floating sum
        pubHub.Publish(PacketSampler.Sum());
    } 

Making the Client

Now let's have a look at the actual HTML/JavaScript code that accomplishes the following:

  1. Renders the gauges using ChartJS library.
  2. Subscribes to our server and updates the gauges when the server tells it to.

We first include various dependencies: Spike-Engine JavaScript SDK and ChartJS (JQuery, Knockout and Globalize are required by ChartJS).

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/globalize/0.1.1/globalize.min.js"></script>
<script src="http://cdn3.devexpress.com/jslib/13.1.6/js/dx.chartjs.js"></script>
<script src="http://cdn.misakai.com/js/2.2.0/spike-sdk.js.src.min.js"></script> 

Once this is done, we can start putting up the layout of the page. Check out the full source of the HTML page to see the code. After that, we need to create the actual Gauges with ChartJS library, this is fairly easy as you just need to define various ranges and visual features. We also set the animationDuration to 200, this is needed so our gauge needle won't take too much time to interpolate between updates.

$("#packetGauge1").dxCircularGauge({
    scale: {
        startValue: 0,
        endValue: 100,
        majorTick: {
            tickInterval: 25
        },
        label: {
            indentFromTick: 8
        }
    },
    margin: {
        left: 10,
        right: 10,
        top: 10,
        bottom: 10
    },

    rangeContainer: {
        width: 4,
        backgroundColor: 'none',
        ranges: [
            {
                startValue: 0,
                endValue: 24,
                color: '#A6C567'
            },
            {
                startValue: 26,
                endValue: 49,
                color: '#A6C567'
            },
            {
                startValue: 51,
                endValue: 74,
                color: '#A6C567'
            },
            {
                startValue: 76,
                endValue: 100,
                color: '#FCBB69'
            }
        ]
    },

    animationDuration: 200,
    animationEnabled: true,
    needles: [{
        offset: 5,
        indentFromCenter: 7,
        value: 0,
        color: '#43474b'
    }],
    spindle: {
        color: '#43474b'
    },

});  

After that, we need to actually perform the connection to our server, here we used the local server (127.0.0.1), one should use the public IP ideally. This code snippet does few things:

  1. It first creates a connection to the remote server.
  2. After that, when the client is connected to the server, it subscribes to PacketWatch hub as we named it.
  3. Next, it hooks up hubEventInform event which is invoked by Spike-Engine every time a message arrives from our PubHub.
  4. Finally, it deserializes the JSON message and updates the gauges.
// When the document is ready, we connect
$(document).ready(function () {
    var server = new ServerChannel("127.0.0.1:8002");

    // When the browser is connected to the server
    server.onConnect(function () {
        server.hubSubscribe('PacketWatch', null);
    });

    // When we got a notification from the server
    server.hubEventInform = function (p) {
        var value = JSON.parse(p.message);
        var count = $('#packetCounter');
        count.text(value);

        var gauge1 = $('#packetGauge1').dxCircularGauge('instance');
        gauge1.needleValue(0, value);

        var gauge2 = $('#packetGauge2').dxCircularGauge('instance');
        gauge2.needleValue(0, value);
    };
}); 

That's it, I hope you like the article. I am looking forward to your remarks or suggestions on how to improve it!

Server Code

Below, for completeness is the full server implementation:

class Program
{
    /// <summary>
    /// Entry point to our console application.
    /// </summary>
    static void Main(string[] args)
    {
        // Start listening on the port 8002
        Service.Listen(
            new TcpBinding(IPAddress.Any, 8002)
            );
    }
 
    /// <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");
 
        // We schedule the OnTick() function to be executed every 200 milliseconds. 
        hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);
    }
 
    /// <summary>
    /// Last packet count.
    /// </summary>
    private static long PacketCount = 0;
 
    /// <summary>
    /// A queue to hold our packets. We need this to calculate a floating sum.
    /// </summary>
    private static Queue<long> PacketSampler
        = new Queue<long>();

    /// <summary>
    /// Occurs when our timer ticks.
    /// </summary>
    private static void OnTick(IHub hub)
    {
        // Cast is as PubHub
        var pubHub = hub as PubHub;
 
        // In this case, 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)
        PacketSampler.Enqueue(packetsDelta);
        if (PacketSampler.Count > 5)
            PacketSampler.Dequeue();
 
        // Publish the floating sum
        pubHub.Publish(PacketSampler.Sum());
    }
}  

Client Code

The full client code can be viewed in the attached archive. Alternatively, I've provided a live demo for this article.  

History

  • 9/7/2013 - Initial article
  • 9/8/2013 - Updated the code & article formatting 

License

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

About the Author

Kel_
Chief Technology Officer Misakai Ltd.
Ireland Ireland
Roman Atachiants is the guy behind www.spike-engine.com project, a real-time, client-server networking layer (SOA, RPC) for .NET developers. Also the founder of Misakai Ltd..
 
He is a software engineer and scientist with extensive experience in different computer science domains, programming languages/principles/patterns & frameworks.
 
His main expertise consists of C# and .NET platform, game technologies, cloud, human-computer interaction, big data and artificial intelligence. He has an extensive programming knowledge and R&D expertise.

 
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 Pinprofessional@AmitGajjar17-Sep-13 18:32 
GeneralMy vote of 5 Pinmemberpwasser8-Sep-13 22:16 
I just like live demos

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140709.1 | Last Updated 9 Sep 2013
Article Copyright 2013 by Kel_
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid