Using the NetAdvantage jQuery Chart and SignalR to Display a Live Data Stream

Graham Murray / Monday, October 31, 2011

The new NetAdvantage jQuery Chart supports many interesting data scenarios, from large volumes, to interpolated animation, to high frequency updating. In this post I will explain how you can update the data assigned to the chart with high frequency creating a "sliding window" effect.

We'll start with building a sample that displays high frequency simulated data in the chart, and then replace the simulated data with an actual live data stream pushed down from the server using SignalR.

Setting up the project

Start by creating an ASP.NET MVC 3 Web Application from the New Project menu.

Select Internet Application, View engine: Razor and hit OK.

Add a reference to the Infragistics.Web.Mvc assembly.

The project template for an MVC 3 Internet Application will have already created a folder named Content and a folder named Scripts in your project. Next, we need to add the appropriate NetAdvantage jQuery bits to these folders.

If you used the default install path for the product, you should find the css files to add to the Content\themes Folder at:

C:\Program Files (x86)\Infragistics\NetAdvantage 2011.2\jQuery\themes\min

And the requisite script files to add to the Scripts\IG folder at:

C:\Program Files (x86)\Infragistics\NetAdvantage 2011.2\jQuery\js\combined\min

Now we can add the requisite script references to _Layout.cshtml to be able to use the Chart from any of the views in the application:

<!-- Required for Chart -->
    <link href="@Url.Content("~/Content/themes/base/jquery-ui.min.css")" rel="stylesheet" type="text/css" />
    <link href=""@Url.Content("~/Content/themes/ig/jquery.ui.custom.min.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.11.min.js")" type="text/javascript"></script>
    <link href="@Url.Content("~/Content/themes/base/ig.ui.chart.min.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/IG/ig.ui.chart.min.js")" type="text/javascript"></script>

Setting up the chart

We are eventually going to be displaying two series of data. One entitled "CPU Usage", and another entitled "Available Memory". Once we factor in SignalR, this data will be pushed down from the server into the chart, but for now, we will simulate the data on the client.

First open up Home\Index.cshtml, as this is where we will add our Chart, and then delete the existing content.

Add a using statement and title to the top:

@using Infragistics.Web.Mvc
@{
    ViewBag.Title = "RealTime chart";
}

Next we will create the data model for the chart. We will be generating the data on the client in the first part of this sample, but the CTP of the Chart still requires us to bind some data to the chart at creation time. In this way, also, I will show how you would bind some data from the server. We will also need the item type later for SignalR

Create a class in the models folder called CPUInfoModel.cs and define it as such:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace RealTimeChart.Models
{
    public class CPUInfoModel
        : List<CPUInfoItem>
    {

    }

    public class CPUInfoItem
    {
        public string DateString { get; set; }
        public double CPUUsage { get; set; }
        public double MemUsage { get; set; }
    }
}

Now we can have the HomeController use this model when rendering Index.cshtml. Open HomeController.cs and edit the Index method to read as such:

        public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";

            return View(new CPUInfoModel());
        }

You will have to add this using statement:

using RealTimeChart.Models;

We are now treating Index.cshtml as a Strong Typed View, so we need to add this statement to the top of the file:

@model RealTimeChart.Models.CPUInfoModel

And now we can define the chart like this:

<style>
    #chart1
    {
        position: relative;
        float: left;
    }
    #legend1
    {
        position: relative;
        float: left;
        margin-left: 15px;
    }
</style>
@(
 Html.Infragistics().DataChart(Model.AsQueryable())
               .ID("chart1")
               .Width("500px")
               .Height("500px")
               .VerticalZoomable(true)
               .HorizontalZoomable(true)
               .WindowResponse(WindowResponse.Immediate)
               .Legend((l) =>
               {
                   l.ID("legend1").Width("150px");
               })
               .Axes((axes) =>
               {
                   axes.CategoryX("xAxis").Label((item) => item.DateString)
                       .LabelExtent(50);
                   axes.NumericY("yAxis")
                       .MinimumValue(0)
                       .MaximumValue(100)
                       .LabelExtent(30);
                   axes.NumericY("yAxis2")
                       .MinimumValue(0)
                       .MaximumValue(4096)
                       .LabelExtent(40)
                       .MajorStroke("transparent")
                       .LabelLocation(AxisLabelsLocation.OutsideRight);
               })
               .Series((series) =>
               {
                   series
                   .Line("series1")
                   .Title("CPU usage")
                   .XAxis("xAxis").YAxis("yAxis")
                   .ValueMemberPath((item) => item.CPUUsage)
                   .Thickness(2);

                   series
                   .Line("series2")
                   .Title("Available Memory")
                   .XAxis("xAxis").YAxis("yAxis2")
                   .ValueMemberPath((item) => item.MemUsage)
                   .Thickness(2);
               }).DataBind().Render()
 )

Let's break this down and discuss what is going on here:

Html.Infragistics().DataChart(Model.AsQueryable())
               .ID("chart1")
               .Width("500px")
               .Height("500px")
               .VerticalZoomable(true)
               .HorizontalZoomable(true)
               .WindowResponse(WindowResponse.Immediate)
               .Legend((l) =>
               {
                   l.ID("legend1").Width("150px");
               })

Here, we are:

  • Binding the chart to the empty collection of items we defined as our model. It will provide a strong typed context by which we can bind to properties of the items later in the chart setup.
  • Setting the id of the DOM element that will be generated for the chart.
  • Setting width and height of the chart to be 500px.
  • Making the chart content zoomable.
  • Ensuring the panning operations are enacted immediately rather than after the user stops manipulating the chart.
  • Setting that the chart should have a legend created, and associated with a DOM element with ID legend1, which should have a width of 150px.

Next we are creating the axes of the chart:

               .Axes((axes) =>
               {
                   axes.CategoryX("xAxis").Label((item) => item.DateString)
                       .LabelExtent(50);
                   axes.NumericY("yAxis")
                       .MinimumValue(0)
                       .MaximumValue(100)
                       .LabelExtent(30);
                   axes.NumericY("yAxis2")
                       .MinimumValue(0)
                       .MaximumValue(4096)
                       .LabelExtent(40)
                       .MajorStroke("transparent")
                       .LabelLocation(AxisLabelsLocation.OutsideRight);
               })

Here we are:

  • Creating a CategoryXAxis identified by the key "xAxis".
    • Telling the axis to use the property DateString for its labels.
    • Setting the amount of vertical space reserved for the x axis to 50 pixels.
  • Creating a NumericYAxis identified by the key "yAxis"
    • Telling the axis to use 0 as the minimum value of its range.
    • Telling the axis to use 100 as the maximum value of its range.
    • Setting the amount of horizontal space reserved for the y axis to 30 pixels.
  • Creating a NumericYAxis identified by the key "yAxis2"
    • Telling the axis to use 0 as the minimum value of its range.
    • Telling the axis to use 4096 as the maximum value of its range.
    • Setting the mount of horizontal space reserved for the y axis to 40 pixels.
    • Hiding the gridlines for this y axis.
    • Telling the axis to render on the outside right edge of the chart.

xAxis is where we will be displaying the date labels for the CPU and Memory data items.

yAxis is where we will be displaying the CPU data, so it is manually ranged from 0 to 100(%). If we did not specify the range, the chart would automatically decide on a range, and this range would be updated as the data in the chart was updated. For our purposes though, it will look much cleaner if the chart does not update the range as the data is changed, and we know the full bounds of the range ahead of time.

yAxis2 is where we will be displaying the Available Memory data, and since the machine I'm using has 4GB of RAM, I am setting the range of this secondary y axis to range from 0 to 4096 for the same reason we are manually specifying the range for the CPU value axis.

The reason we are using two separate y axes is because otherwise it would be hard to see the detail on the CPU series. Its values only vary from 0 to 100, which is only a small portion of the Memory data's scale. Furthermore, we are really only interested in temporally comparing the shape of these two series, not comparing their absolute values to each other.

Next, we are creating the series that will be displayed in the chart:

               .Series((series) =>
               {
                   series
                   .Line("series1")
                   .Title("CPU usage")
                   .XAxis("xAxis").YAxis("yAxis")
                   .ValueMemberPath((item) => item.CPUUsage)
                   .Thickness(2);

                   series
                   .Line("series2")
                   .Title("Available Memory")
                   .XAxis("xAxis").YAxis("yAxis2")
                   .ValueMemberPath((item) => item.MemUsage)
                   .Thickness(2);
               }).DataBind().Render()

Here we are:

  • Creating a series to display the CPU usage information.
    • Setting the series to be a line series identified by the key "series1"
    • Setting the series title (to use in the Legend) as "CPU usage"
    • Telling the series to use an x axis identified by the key "xAxis" and a y axis identified by the key "yAxis"
    • Telling the series that its values will be in the CPUUsage property of the data items.
    • Setting the thickness of the line for the series to be 2 pixels.
  • Creating a series to display the Available Memory information.
    • Setting the series to be a line series identified by the key "series2"
    • Setting the series title (to use in the Legend) as "Available Memory"
    • Telling the series to use an x axis identified by the key "xAxis" and a y axis identified by the key "yAxis"
    • Telling the series that its values will be in the MemUsage property of the data items.
    • Setting the thickness of the line for the series to be 2 pixels.
  • Telling the chart to bind against the provided data collection and then render to the view.

There is, of course, no data, because all we have provided is an empty collection. But we can see the chart and its relevant y axes, which can be displayed because they have been manually ranged.

Please note, when running the project from here on in, please "Start Without Debugging" (Ctrl-F5) If you run with the debugger attached, it will hinder the performance and you won't see the Chart performing at its best. Also, if you are using Chrome v15, it appears they have currently done something to severely hinder their canvas rendering performance. Hopefully they will resolve it in Chrome 16, but until then, you will be better off using a different browser, or running Chrome with this command line argument: --disable-accelerated-2d-canvas

Adding the Simulated Real-Time data

Now that we have the chart configured we can introduce the simulated real-time data into the situation. Insert this code below the style definition in Index.cshtml:

<script type="text/javascript">
    $(function () {
        var queued = 0, data = [], updateData, currCPU = 10.0, currMem = 1024, connection;

        $("#chart1").igDataChart("option", "dataSource", data);
        $("#chart1").igDataChart("option", "series", [{
            name: "series1", dataSource: data
        }, {
            name: "series2", dataSource: data
        }]);
        $("#chart1").igDataChart("option", "axes", [{
            name: "xAxis", dataSource: data
        }]);

        var updateData = function (newItem) {
            data.push(newItem);
            $("#chart1").igDataChart("notifyInsertItem", "series1", data.length - 1, newItem);
            queued++;
            if (queued > 2000) {
                oldItem = data[0];
                data.shift();
                $("#chart1").igDataChart("notifyRemoveItem", "series1", 0, oldItem);
                queued--;
            }
        }

        var generateRandomItem = function () {
            var newItem = {}, currDate = new Date(),
            hours = currDate.getHours(), minutes = currDate.getMinutes(),
            seconds = currDate.getSeconds();

            if (Math.random() > .5) {
                currCPU += Math.random() * 2.0;
            } else {
                currCPU -= Math.random() * 2.0;
            }

            if (Math.random() > .5) {
                currMem += Math.random() * 5.0;
            } else {
                currMem -= Math.random() * 5.0;
            }

            if (currMem <= 0) {
                currMem = 0;
                currMem += Math.random() * 5.0;
            }

            if (currMem > 4096) {
                currMem = 4096;
                currMem -= Math.random() * 5.0;
            }

            if (currCPU <= 0) {
                currCPU = 0;
                currCPU += Math.random() * 2.0;
            }

            if (currCPU > 100) {
                currCPU = 100;
                currCPU -= Math.random() * 2.0;
            }

            if (hours > 12) {
                hours = hours - 12;
            }
            if (hours < 10) {
                hours = "0" + hours;
            }
            if (minutes < 10) {
                minutes = "0" + minutes;
            }
            if (seconds < 10) {
                seconds = "0" + seconds;
            }

            newItem.DateString = hours + ":" + minutes + ":" + seconds;
            newItem.CPUUsage = currCPU;
            newItem.MemUsage = currMem;

            return newItem;
        }

        window.setInterval(function () {
            var newItem = generateRandomItem();

            updateData(newItem);
        }, 33);
    });
</script>

Let's again break this down and take a look at what's going on.

var queued = 0, data = [], updateData, currCPU = 10.0, currMem = 1024, connection;

        $("#chart1").igDataChart("option", "dataSource", data);
        $("#chart1").igDataChart("option", "series", [{
            name: "series1", dataSource: data
        }, {
            name: "series2", dataSource: data
        }]);
        $("#chart1").igDataChart("option", "axes", [{
            name: "xAxis", dataSource: data
        }]);

Here, we are defining a new array that we can add values to on the client side, and binding it to the various series and axes in the chart. In the future, we will allow you to access the array already bound to the chart during the render, but in the CTP there are a few known issues with that scenario, so we are instead replacing it with our own array on the client.

var updateData = function (newItem) {
            data.push(newItem);
            $("#chart1").igDataChart("notifyInsertItem", "series1", data.length - 1, newItem);
            queued++;
            if (queued > 2000) {
                oldItem = data[0];
                data.shift();
                $("#chart1").igDataChart("notifyRemoveItem", "series1", 0, oldItem);
                queued--;
            }
        }

This method will add a new item to the array bound to the chart, and then notify the chart that the array has a new item. When the array has more than 2000 values in it, we will start removing items from the head of the array and notifying the chart of these removals. In this way we can create a sliding window style display of the data.

var generateRandomItem = function () {
            var newItem = {}, currDate = new Date(),
            hours = currDate.getHours(), minutes = currDate.getMinutes(),
            seconds = currDate.getSeconds();

            if (Math.random() > .5) {
                currCPU += Math.random() * 2.0;
            } else {
                currCPU -= Math.random() * 2.0;
            }

            if (Math.random() > .5) {
                currMem += Math.random() * 5.0;
            } else {
                currMem -= Math.random() * 5.0;
            }

            if (currMem <= 0) {
                currMem = 0;
                currMem += Math.random() * 5.0;
            }

            if (currMem > 4096) {
                currMem = 4096;
                currMem -= Math.random() * 5.0;
            }

            if (currCPU <= 0) {
                currCPU = 0;
                currCPU += Math.random() * 2.0;
            }

            if (currCPU > 100) {
                currCPU = 100;
                currCPU -= Math.random() * 2.0;
            }

            if (hours > 12) {
                hours = hours - 12;
            }
            if (hours < 10) {
                hours = "0" + hours;
            }
            if (minutes < 10) {
                minutes = "0" + minutes;
            }
            if (seconds < 10) {
                seconds = "0" + seconds;
            }

            newItem.DateString = hours + ":" + minutes + ":" + seconds;
            newItem.CPUUsage = currCPU;
            newItem.MemUsage = currMem;

            return newItem;
        }

This code will just generate us some randomly fluctuating data items so that we can stream simulated data into the chart. It relates the new values to the old values so that we get a line shape rather than completely random values.

window.setInterval(function () {
            var newItem = generateRandomItem();

            updateData(newItem);
        }, 33);

Finally, here we are going to generate a new item and add it to the chart every 33 milliseconds.

If you run the project now, you should see the data beginning to fill up in the chart until it reaches 2000 items and then the values will start to slide across the screen as values are removed from the head of the series. You can zoom in and out (scroll wheel, page-up, page-down, arrow keys, home, etc.) for more detail. Notice the performance remains fluid as you zoom in and out even as points are being subtracted and added to the chart at high frequency.

Adding real live data to chart

This is very neat, but we'd rather see some real data in the chart, right?

SignalR is a cool library for ASP.NET and JavaScript for enabling neat real time web application scenarios. We will use it to extend our sample and push live CPU and Memory data down to the chart.

First, we need to add SignalR to the project. It's available on NuGet so do a right click on the project node and select Manage NuGet Packages.

In the resulting Dialog, select Online and then enter SignalR in the search field. Select SignalR and then hit Install.

SignalR requires a more recent version of jQuery than is included in an MVC project by default, so it will add jquery-1.6.4.js to the Scripts folder, and will likely update jQuery UI to 1.8.16, so we need to replace the existing references to jQuery in _Layout.cshtml with:

<script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-ui-1.8.16.min.js")" type="text/javascript"></script>

And then also add the reference to SignalR:

<script src="@Url.Content("~/Scripts/jquery.signalR.min.js")" type="text/javascript"></script>

Once this is set up we can begin to create the SignalR implementation of the live data.

Generating the Live Data

We are going to use the low level API in SignalR and implement a PersistentConnection which we will use to stream data to any JavaScript clients that connect to the stream. Let's create a new class in the project and call it CPUDataStream.cs.

Copy these contents into the file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using SignalR;

namespace RealTimeChart
{
    public class CPUDataStream
        : PersistentConnection
    {
    }
}

This is as simple a SignalR connection as we can create. We are simply going to use it to broadcast data to any clients that are connected. It doesn’t need to respond to any direct queries.
To make this SignalR PersistentConnection accessible to the client, we need to use the SignalR API to add a route to the connection. Open Global.asax.cs and add this line to the beginning of the Application_Start method:

RouteTable.Routes.MapConnection<CPUDataStream>("cpuDataStream", "cpuDataStream/{*operation}");

You will need to add these to the using statements:

using SignalR.Routing;
using System.Threading;
using SignalR;
using System.Diagnostics;
using RealTimeChart.Models;

This has added a route such that /cpuDataStream/* should all be directed to our CPUDataStream class for processing.
Now, we just need to set up the application to broadcast the data we want on this connection, so add the following to the end of the Application_Start method:

ThreadPool.QueueUserWorkItem(_ =>
            {
                var connection = Connection.GetConnection<CPUDataStream>();
                var counter = new PerformanceCounter();
                counter.CategoryName = "Processor";
                counter.CounterName = "% Processor Time";
                counter.InstanceName = "_Total";

                var memCounter = new PerformanceCounter();
                memCounter.CategoryName = "Memory";
                memCounter.CounterName = "Available MBytes";


                while (true)
                {
                    var item = new CPUInfoItem()
                    {
                        DateString = DateTime.Now.ToString("hh:mm:ss"),
                        CPUUsage = Math.Round(counter.NextValue(), 2),
                        MemUsage = memCounter.NextValue()
                    };

                    connection.Broadcast(item);

                    Thread.Sleep(150);
                }
            });

This will create a work item in the thread pool that will post an updated reading of the current CPU usage and available memory to our connection every 150ms. Using 150ms may not scale very well in real world scenarios because the current version of SignalR resorts to HTTP long polling to push data to the clients (it seems like future versions will try WebSockets, if available). For the purposes of this demo, however, we will set a high refresh rate, so it will look cooler. You can try even lower refresh intervals if you want, and see how far you can push it. The chart should be able to handle it, refreshing the HTTP connection will start to cause issues, though, after a point.

Consuming the live data from the Chart

The only remaining piece we need to add is the client side code to connect to the streaming data. So insert this code into the bottom of the script block we created before:

connection = $.connection('/cpuDataStream');

        connection.received(function (dataItem) {
            var newItem = dataItem;

            updateData(newItem);
        });

        connection.start();

And comment out the window.setInterval call that we had before:

//        window.setInterval(function () {
//            var newItem = generateRandomItem();

//            updateData(newItem);
//        }, 33);

It is showing a live feed of the CPU and memory usage on the server. Pretty cool huh?

You can download the project used in this article here.