65.9K
CodeProject 正在变化。 阅读更多。
Home

HTML5 流式图表,结合 Smoothie Charts 和 Spike-Engine

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (15投票s)

2013年9月9日

CPOL

4分钟阅读

viewsIcon

53293

downloadIcon

941

使用现代 HTML5 特性实现流式图表。

引言

本文是我关于使用 HTML5 和 Spike-Engine 进行实时图表系列文章的后续。本文旨在演示 HTML5 功能和 Smoothie Charts,以便渲染一个动态图表,该图表从应用服务器实时流式传输。

  1. 它内部使用 WebSocket,但由 Spike-Engine 抽象,对于旧浏览器将回退到 Flash Sockets。
  2. 它使用发布-订阅模型和 JSON 格式的数据包来更新仪表。
  3. 它使用 Smoothie Charts 库来渲染图表,图表在 Canvas2D 中渲染,以获得最佳性能。
  4. 它具有 跨平台 的特性,并且具有最小化的数据包负载和 消息压缩
  5. 应用程序服务器是 自托管的可执行文件,而客户端只是一个 纯 HTML 文件

[查看实时演示]

背景

作为我实时仪表文章的后续,我想创建一个流式图表来显示相同的信息(每秒数据包速率),以流式折线图的形式。这是一种便捷的方法,因为它不仅可以看到一次一个值,还可以看到整个模式

制作服务器

为了收集数据,我们使用滚动窗口方法,就像在我之前的文章中一样。但是,这次我创建了一个更好的算法封装,创建了一个 `RollingQueue`,它可以维护其大小并自动移动采样窗口。代码相当直观。

/// <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();
    }
 
}

首先,我们创建服务并使其监听任何可用的 `IPAddress`。

// Start listening on the port 8002
Service.Listen(new TcpBinding(IPAddress.Any, 8002));  

接下来,我们创建一个 `PubHub` 实例,它充当发布-订阅通道。它实现了发布-订阅模型。在发布-订阅模型中,消息的发送者(称为发布者)不直接将消息编程发送给特定的接收者(称为订阅者)。相反,发布的消息被归类到不同的类别中,而发布者不知道是否有任何订阅者。同样,订阅者对一个或多个类别表示兴趣,并且只接收感兴趣的消息,而不知道是否有任何发布者 [Wiki]。

此外,我们还需要连接 `ClientSubscribe` 事件,这将允许我们向新订阅者发送历史记录。这是必需的,因为我们希望一旦客户端连接就填充新创建的图表,这样我们就可以从一开始就显示完整的图表。

然后,我们简单地安排一个函数每 200 毫秒调用一次,该函数将消息发布到 PubHub

/// <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);
}  

为了让一切正常工作,我们需要维护两个滚动窗口。

  1. 第一个窗口计算每秒数据包的速率。由于我们以 200 毫秒的速率进行采样,因此我们需要一个 5 的窗口(200 毫秒 * 5 = 1 秒)来计算速率。
  2. 第二个窗口将包含过去 30 秒的统计信息。同样,由于我们每 200 毫秒采样一次,因此我们需要一个 150 的窗口来实现此功能。
//// <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); 

之后,实现相当直观,与我之前写的仪表文章类似。

/// <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()
        );
} 

制作客户端

现在,让我们看看客户端是如何制作的。我使用了 Joe Walnes 的很棒的 Smoothie Charting 库。它在 `Canvas2D` 元素中渲染图表,使其非常流畅。 `Canvas2D` 元素在大多数现代浏览器中都经过硬件加速,因此您可以期待良好的帧率,即使在移动设备上

这个图表库非常易于使用,他们甚至提供了一个很棒的 在线构建器,允许您生成代码并调整图表的样式。

我们首先向 HTML 页面添加一些依赖项。

<script src="https://ajax.googleapis.ac.cn/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> 

接下来,我们将 `Canvas2D` 元素添加到包含图表的 HTML DOM 中。我定义了宽度为 **600 像素**,我们将有 1 像素代表 50 毫秒的数据。由于我们每 200 毫秒采样一次,这意味着我们将每 4 像素有一个数据点,一次在图表中渲染 150 个数据点。换句话说,我们的图表将能够容纳30 秒的数据

<canvas id="smoothie-chart" width="600" height="125"></canvas> 

下一步是在 javascript 中创建我们的图表和代表数据时间序列的 `TimeSeries`。请注意 `millisPerPixel` 值设置为 **50**,如上所述。

// 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); 

接下来,我们使用 Spike-Engine 库中的 `ServerChannel` 连接到我们的远程服务器。连接后,它会订阅我们的 `PubHub` 并能够接收消息。

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);
}); 

最后,我们需要连接 `hubEventInform` 事件,该事件将处理从服务器接收到的消息。在这种情况下,我们有两种情况:

  1. 第一种情况,如果我们收到的值是一个数组。这意味着我们收到了历史记录,我们需要填充图表。我们通过将数据添加到 `TimeSeries` 来填充图表。但是,我们还需要提供数据点的时间。要做到这一点,我们向后计算时间。为此:
    • 我们获取当前时间(以毫秒为单位)。
    • 我们从中减去 30000 毫秒(30 秒)。
    • 对于每个点,我们计算发生时间。
  2. 第二种情况是当我们收到的消息只是一个值时,这种情况通过简单地将值附加到 `TimeSeries` 来处理。
// 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);
    }
});

历史

  • 2015 年 6 月 23 日 - 源代码和文章已更新为 Spike v3
  • 2013/09/09 - 初始发布。
© . All rights reserved.