Nothing screams PRODUCTIVITY like real-time charts on a large dashboard, with every onscreen update beating to the heartbeat of your organization’s rhythm.

In this post, I created a real-time chart using ASP.NET Core, SignalR, and the JavaScript library Chart.js, and as you’ll see, it was all relatively straightforward.

Setting Up Our Chart Host

The first step was to create an ASP.NET Core web application, and in my case, I started with the Empty template but also decided to use a Razor Page as my default page. You’ll also need to add a wwwroot folder so you can serve some JavaScript files.

Once you’ve created your host application, I recommend using Library Manager or NPM to pull down the web dependencies of SignalR and Chart.JS. You can also download the latest manually, but I recommend reading my Library Manager (LibMan) post. Here’s my libman.json file, which you should place at the root of your web application.

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "Chart.js@3.9.1",
      "destination": "wwwroot/lib/chartjs"
    },
    {
      "library": "microsoft-signalr@6.0.8",
      "destination": "wwwroot/lib/microsoft-signalr"
    }
  ]
}

Running dotnet libman restore will pull down the JavaScript dependencies and place them in the wwwroot folder under the paths described previously by the JSON file.

Great! Now you’re ready to start programming.

Generating Random Points Of Data

You’ll use a Buffer class for this sample to hold our streaming data. In an actual application, your real-time data will likely be coming from a database or third-party service. Therefore, the Buffer class will only ever hold a certain amount of values, discarding the oldest element in favor of a new one. A word of warning, this type is not thread-safe. Use it with caution.

/// <summary>
/// https://stackoverflow.com/questions/12294296/list-with-limited-item
/// </summary>
/// <typeparam name="T"></typeparam>
public class Buffer<T> : Queue<T>
{
    public int? MaxCapacity { get; }
    public Buffer(int capacity) { MaxCapacity = capacity; }
    public int TotalItemsAddedCount { get; private set; }

    public void Add(T newElement)
    {
        if (Count == (MaxCapacity ?? -1)) Dequeue();
        Enqueue(newElement);
        TotalItemsAddedCount++;
    }
}

Your Buffer instance will be of type Point, with each point having a Value and a Label. This data structure will be important later for your chart.

public record Point(string Label, int Value);

Let’s also create an extension method to add random items to your Buffer using the current date and time.

public static class BufferExtensions
{
    public static Point AddNewRandomPoint(this Buffer<Point> buffer)
    {
        var now = DateTime.Now.AddMonths(buffer.TotalItemsAddedCount);
        var year = now.Year;
        var monthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(now.Month);
        var point = new Point($"{monthName} ({year})", RandomNumberGenerator.GetInt32(1, 11));
        buffer.Add(point);
        return point;
    }
}

Finally, you’ll register and prime the initial data set with reasonable data points. Then, you’ll register a singleton instance of Buffer<Point> as part of ASP.NET Core’s services.

// our data source, could be a database
builder.Services.AddSingleton(_ => {
    var buffer = new Buffer<Point>(10);
    // start with something that can grow
    for (var i = 0; i < 7; i++) 
        buffer.AddNewRandomPoint();

    return buffer;
});

A single instance will let us access it from anywhere in our application and maintain the same data.

Chart Hub and SignalR

Step one is registering all the SignalR services in your ASP.NET Core application. That’s as simple as a single line of code in your Program.cs file.

builder.Services.AddSignalR();

You need to register a Hub, which allows your clients to connect to your ASP.NET Core application. Since your server will only be calling your clients, and not the other way around, you can have a simple Hub implementation.

using Microsoft.AspNetCore.SignalR;

namespace Charts.Hubs;

public class ChartHub : Hub
{
    public const string Url = "/chart";
}

Then, you’ll need to map the hub as a SignalR endpoint. In your Program.cs file, add the following line to your ASP.NET Core pipeline.

app.MapHub<ChartHub>(ChartHub.Url);

That’s it for setting up our SignalR infrastructure.

Generating Data Points On An Interval

Now that you can generate random data points, you need a service worker to call the random generator method and call the connected clients. Now, create a class called ChartValueGenerator with the following implementation.

using Charts.Hubs;
using Microsoft.AspNetCore.SignalR;

namespace Charts.Services;

public class ChartValueGenerator : BackgroundService
{
    private readonly IHubContext<ChartHub> _hub;
    private readonly Buffer<Point> _data;

    public ChartValueGenerator(IHubContext<ChartHub> hub, Buffer<Point> data)
    {
        _hub = hub;
        _data = data;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _hub.Clients.All.SendAsync(
                "addChartData",
                _data.AddNewRandomPoint(), 
                cancellationToken: stoppingToken
            );

            await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
        }
    }
}

Note that the background service takes two dependencies: IHubContext<ChartHub> and Buffer<Point>. The service will call all clients every two seconds and give them a new data point.

Implementing the Razor Page For Chart.Js

Now that you have a service generating random data points and a Buffer of points ready to graph, you need to create a Razor page to display the chart appropriately.

First, let’s look at your Razor page HTML.

@page
@using Charts.Services
@model Charts.Pages.Index
@inject Buffer<Point> Buffer

@{
    Layout = null;
    var labels = Buffer.Select(x => x.Label);
    var data = Buffer.Select(x => x.Value);
}

<!DOCTYPE html>

<html lang="en">
<head>
    <title>Charts with Chart.Js and SignalR</title>
</head>
<body>
<div>
    <div style="width: 400px; height: 400px">
        <canvas id="myChart" width="400" height="400"></canvas>
    </div>
    <script id="data" type="application/json">
    @Json.Serialize(
        new
        {
            labels,
            limit = Buffer.MaxCapacity.GetValueOrDefault(10),
            url = "/chart",
            datasets = new object[]
            {
                new
                {
                    label = "SignalR Dataset",
                    data,
                    fill = false,
                    borderColor = "rgb(75, 192, 192)",
                    tension = 0.1
                }
            }
        })    
    </script>
</div>
</body>
<script src="~/lib/microsoft-signalr/signalr.min.js"></script>
<script src="~/lib/chartjs/chart.min.js"></script>
<script defer src="~/js/index.js"></script>
</html>

You’ll be doing a few things at this stage of building your application:

You inject the Buffer<Point> instance into the page for that initial page load. Create a canvas element, which will hold our chart. You configure the chart.js component with data by serializing the data from the buffer instance. You add script references to SignalR and Chart.js. Adding script tags is likely better done in a _Layout.cshtml file but is done here for clarity. You’ll reference your index.js file and defer the file loading. Deferring the script ensures the script only executes once the page is ready.

Most important is our script tag with the identifier of data. You’ll use this to initialize our chart from JavaScript.

Writing our Chart.JS JavaScript

You’ll want to create a new index.js file under wwwroot/js/. Once created, you can paste the following JavaScript.

const data = JSON.parse(document.getElementById('data').innerHTML);
const ctx = document.getElementById('myChart').getContext('2d');
const myChart = new Chart(ctx, {
    type: 'line',
    data: data,
    options : {
        scales: {
            y : {
                suggestedMax: 10,
                suggestedMin: 1
            }
        }
    }
});

const connection = new signalR.HubConnectionBuilder()
    .withUrl(data.url)
    .configureLogging(signalR.LogLevel.Information)
    .build();

async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
}

connection.onclose(async () => {
    await start();
});

connection.on("addChartData", function(point) {
    
    myChart.data.labels.push(point.label);
    myChart.data.datasets.forEach((dataset) => {
        dataset.data.push(point.value);
    });

    myChart.update();

    if (myChart.data.labels.length > data.limit) {
        myChart.data.labels.splice(0, 1);
        myChart.data.datasets.forEach((dataset) => {
            dataset.data.splice(0, 1);
        });
        myChart.update();
    }
});

// Start the connection.
start().then(() => {});

This code does two important things as it relates to the chart:

It connects to the SignalR hub, allowing your server-side service to call the connected client. It reads the initial data from our data element and initializes the chart.

You will use a line chart, but feel free to play around with all the options Chart.Js offers.

The Whole Program

Before running your application, I’ll include the entire contents of Program.cs here, but you can also get a complete version of this demo at my GitHub repository.

using Charts.Hubs;
using Charts.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

// our data source, could be a database
builder.Services.AddSingleton(_ => {
    var buffer = new Buffer<Point>(10);
    // start with something that can grow
    for (var i = 0; i < 7; i++) 
        buffer.AddNewRandomPoint();

    return buffer;
});

builder.Services.AddHostedService<ChartValueGenerator>();

var app = builder.Build();

app.UseStaticFiles();
app.MapRazorPages();
app.MapHub<ChartHub>(ChartHub.Url);

app.Run();

Running the Application

Now that you have a full ASP.NET Core, SignalR, and Chart.JS demo written, there’s one thing left to do. Run it!

chart in action

You should be seeing a living, breathing chart in your browser. How awesome is that?!

Conclusion

ASP.NET Core, SignalR, and Chart.Js demo on GitHub

For this tutorial, I built a real-time chart using SignalR, which was surprisingly simple. A testament to where we are with technologies like ASP.NET Core, SignalR, and Chart.Js. It all bolts together nicely to create a fantastic user experience.

If you enjoyed this tutorial, please share it with your friends and coworkers. Also, remember to follow me on Twitter at @buhakmeh for the latest tips and tricks and #dotnet news. As always, thank you.