다음을 통해 공유


Tutorial: Creating Charts with Real-Time Data

Applies to: Functional Programming

Authors: Tomas Petricek and Jon Skeet

Referenced Image

Get this book in Print, PDF, ePub and Kindle at manning.com. Use code “MSDN37b” to save 37%.

Summary: This tutorial demonstrates how to dynamically update a chart to display real-time data. It discusses how to use F# asynchronous workflows and how to receive updates on a background thread.

This topic contains the following sections.

  • Working with Real-Time Data
  • Displaying CPU Usage with a Line Area Chart
  • Retrieving Data in the Background
  • Summary
  • Additional Resources
  • See Also

This article is associated with Real World Functional Programming: With Examples in F# and C# by Tomas Petricek with Jon Skeet from Manning Publications (ISBN 9781933988924, copyright Manning Publications 2009, all rights reserved). No part of these chapters may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

Working with Real-Time Data

This tutorial shows how to create a chart that displays real-time data. It can be used as a guide when creating an application that monitors a frequently changing web site, displays data obtained from sensors, or visualizes stock prices, and so on. When using Microsoft Chart Controls, it is possible to continue adding data while the chart is displayed. The only limitation is that a chart can be accessed only from the thread where it was created. There are also certain types of charts that are particularly suitable for visualizing large amounts of data using points or lines. These may be useful when the source generates a large number of inputs.

In this tutorial, you’ll learn everything you need to display real-time data:

  • Use asynchronous workflows running on a single thread to update a chart repeatedly

  • Update the range of x- and y-axes depending on the values being added to a chart

  • Use asynchronous workflows to generate values in the background

  • Create charts suitable for efficiently displaying a large number of data points

The tutorial walks through the implementation of two examples that demonstrate working with real-time data. The first example creates a chart that displays CPU usage. The second example generates data using a random walk algorithm and demonstrates a frequently updated chart with a large number of points.

Displaying CPU Usage with a Line Area Chart

When creating a Windows Forms application, the controls of the application can be accessed only from the thread where the user interface was constructed. This thread runs a Windows message loop, so all user interface events are also triggered on this thread. For this reason, event handlers can safely update the user interface.

An event handler shouldn't run long running operations because that would make the user interface non-responsive. Obtaining CPU usage can be done quite efficiently, so the example doesn't need any additional threads. It can simply run the whole application on the single GUI thread. This can be done either using a Timer object from the System.Windows.Forms namespace which uses the Windows message queue to report the events or using asynchronous workflows and the F# Async.StartImmediate primitive. The primitive ensures that all of the workflow code is executed on the GUI thread. The tutorial follows the latter approach. However, the first snippet shows a function that gets the current CPU usage:

open System.Diagnostics

/// Function that returns the current CPU usage
let getCpuUsage = 
    let counter = 
        new PerformanceCounter
          ( CounterName = "% Processor Time", 
            CategoryName = "Processor", InstanceName = "_Total" )
    (fun () -> counter.NextValue())

The implementation of the getCpuUsage function first initializes a performance counter. Links to articles with more information about performance counters (and the names used) can be found at the end of the article. After creating the counter, the code creates a function using a lambda expression. This way, the value counter is private to the function but is not recreated each time the function is called. The snippet sets all of the properties that are needed to identify the counter in the system when creating the counter. The current value of the counter can be obtained using the NextValue member.

The next step is to create objects representing the chart. The tutorial uses a helper function createChart that is implemented in the previous tutorial (Tutorial: Creating a Series Using Data Binding) and it can also be obtained from the complete source code available at the end of the article. The following snippet configures the chart to use a spline area chart (which fills an area under a smooth line):

let chart, series = createChart SeriesChartType.SplineArea
let area = chart.ChartAreas.[0]
area.BackColor <- Color.Black
area.AxisX.MajorGrid.LineColor <- Color.DarkGreen
area.AxisY.MajorGrid.LineColor <- Color.DarkGreen
chart.BackColor <- Color.Black
series.Color <- Color.Green

The code creates a chart with black background, dark green grid, and light green filled area that shows the CPU usage, which is a common look for this kind of a technical chart. The createChart function creates a chart with a single area but it doesn't return a reference to the area. Instead, the snippet needs to get the area from the ChartAreas collection.

The last piece of code that needs to be written is the most interesting part. It creates an asynchronous workflow that periodically updates the chart area. The implementation uses a while loop in the workflow and the workflow is started such that all of the user code runs on the main GUI thread (meaning that it can safely access the series object):

let updateLoop = async { 
    while not chart.IsDisposed do
        let v = float (getCpuUsage()) 
        series.Points.Add(v) |> ignore
        do! Async.Sleep(250) }

Async.StartImmediate updateLoop

The loop runs until the chart is disposed (i.e., when the form is closed by the user). The body obtains the current CPU usage and adds it as a new data point in the series. The Add method creates a new DataPoint object and returns it as the result. The snippet doesn't need it, so it uses the ignore function. Adding the new point immediately redraws the chart area, so this is all that needs to be done. After adding the point, the workflow calls Async.Sleep to suspend itself for a quarter of a second before going back through the while loop.

Updating the chart can be done only from the main GUI thread. When running the data retrieval in the background, the code should then schedule an update of the chart series on the main thread. If the retrieval doesn’t take a long time, the entire workflow can run on the GUI thread. The listing above chooses the latter approach, which can be implemented by starting the workflow using Async.StartImmediate. The CPU usage graph created in this section is shown in Figure 1.

Figure 1. A graph showing real-time CPU usage data

Referenced Image

The chart created in this section is dynamically updated, but it is only adding new points. After running for a long time, it will become visually crowded. The next section discusses another example. It retrieves data on the background thread and also shows how to keep only a certain number of data points in the chart.

Retrieving Data in the Background

This example implements a workflow that calculates a random walk and displays the generated values. The calculation is run on a background thread, so the approach is suitable for more complicated calculations that take some time to complete. The example uses the FastLine chart type. This type of chart is a line chart that is optimized for a very large number of data points. It doesn’t support some additional features that usual Line chart does such as labels, shadows, and visual properties of individual points. On the other hand, the series can be drawn more efficiently. A related data series type for charting large amounts of data is FastPoints, which displays data elements as dots.

let chart, series = createChart SeriesChartType.FastLine
let axisX = chart.ChartAreas.[0].AxisX
let axisY = chart.ChartAreas.[0].AxisY
chart.ChartAreas.[0].InnerPlotPosition <- 
    new ElementPosition(10.0f, 2.0f, 85.0f, 90.0f)

The listing first creates a chart and gets a reference to objects representing the X and y-axes. These two objects will be needed quite frequently in the rest of the code, so this makes the code below easier to read. Finally, the snippet also specifies the InnerPlotPosition property of the chart area. This defines the location where the content of the chart is located. The numbers specify the offsets from the left and the top together with the width and the height in percent of the overall control size.

A fixed location is needed because the example dynamically changes the range of the axes, and the length of the labels associated with the axes will change (e.g., “5” and “1.5”). Automatic positioning would result in a jittery chart area as the chart resizes to accommodate the different label sizes.

The ranges need to be updated by the program because the example also removes points from the data series. In this case, the range stops automatically updating after points are removed. The following function takes the number of points generated so far as the argument. It finds the maximum and minimum Y values in the series and updates the ranges:

let updateRanges(n) =    
  let values = seq { for p in series.Points -> p.YValues.[0] }
  axisX.Minimum <- float n - 500.0
  axisX.Maximum <- float n 
  axisY.Minimum <- values |> Seq.min |> Math.Floor
  axisY.Maximum <- values |> Seq.max |> Math.Ceiling

The example keeps the last 500 points in the chart series. The values on the x-axis are the automatically generated numbers that are incremented each time a point is added. When setting the range of the x-axis, the maximum value is the number of points generated so far and the minimum value is 500 less than that. To find the maximum and minimum Y values, the snippet first creates a sequence with all Y values and then uses Seq.min and Seq.max to find the minimum and maximum. To add some space around the series and to ensure that the labels show readable numbers, the minimum is rounded down and the maximum is rounded up.

The next step in the tutorial looks at adding new data points to the chart. As in the previous example, the computation that generates data and updates the chart is written as an asynchronous workflow. The example shows how to implement the workflow when the calculation takes some time. The calculation of random walk doesn't take a long time so the function artificially blocks using Sleep but the approach would work for CPU-intensive computations. This is possible because the computation is run in a background thread. However, the chart needs to be updated from the GUI thread. This can be done by switching between the threads.

The following asynchronous function switches to the GUI thread, checks if the chart is still open, updates the chart, and then switches back to a background thread. It also returns a Boolean value indicating whether or not the chart has been disposed, so that the calling workflow can terminate when the chart is closed:

open System.Threading

let ctx = SynchronizationContext.Current

let updateChart(valueX, valueY) = async {
    do! Async.SwitchToContext(ctx)
    if chart.IsDisposed then 
        do! Async.SwitchToThreadPool()
        return false
    else  
        series.Points.AddXY(valueX, valueY) |> ignore
        while series.Points.Count > 500 do series.Points.RemoveAt(0)
        updateRanges(valueX)
        do! Async.SwitchToThreadPool()
        return true }

The snippet starts by creating a value ctx that keeps the synchronization context of the main GUI thread. The updateChart function takes the X and Y values as arguments and starts by switching to the GUI context, so that the code can safely access chart and series objects. If the form has been closed, the workflow switches back to the thread pool (although this isn’t strictly necessary because the workflow will terminate anyway) and returns false. If the form is still open, the function adds the new point and removes the oldest points until the count is at most 500. Then it updates the ranges of the axes, switches back to the thread pool and returns true, so that the workflow continues running.

The only remaining piece of code to make the sample complete is a loop that generates data and updates the chart until the form is closed. The loop needs to keep some state, so it is written as a recursive asynchronous workflow. The state consists of the number of generated data points and the value of the last point:

let randomWalk =
    let rnd = new Random()
    let rec loop(count, value) = async {
        let count, value = count + 1, value + (rnd.NextDouble() - 0.5)
        Thread.Sleep(20)
        let! running = updateChart(float count, value)
        if running then return! loop(count, value) }
    loop(0, 0.0)

Async.Start(randomWalk)

The loop is written by an inner recursive function named loop that keeps the state of the process. The random walk calculation adds a random number (in a range -0.5 to +0.5) to the current value in every step. After doing that, the workflow calls the Thread.Sleep function to block the thread for some time. Note that the example actually blocks the thread (simulating some CPU-intensive calculation), so it is important that the workflow is executed on a background thread. Note that this is very different than the previous example, which used Async.Sleep, which suspends the workflow, but doesn't block an actual thread.

Next, the workflow uses let! to call the asynchronous function updateChart. The function switches to the GUI thread, updates the chart, and, before returning, it switches back to a background thread. The result of the call is used to decide whether the workflow should continue looping or not. Finally, the snippet starts the workflow using the Async.Start function. This differs from StartImmediate (used in a previous example) in that it starts the computation in a background thread. The screenshot in Figure 2 shows the result.

Figure 2. A chart showing random walk generated in background

Referenced Image

Summary

This tutorial discussed several topics that are important when using Microsoft Chart Controls to display real-time data. The first example looked at a scenario where the data can be retrieved efficiently so the processing can be performed on the GUI thread. This pattern can be elegantly implemented by starting an asynchronous workflow using Async.StartImmediate.

The second example created a more advanced application that also removes old data points. When doing that, the code also needs to explicitly calculate the range of the axes (if the ranges change dynamically). In that case, it is useful to explicitly specify the inner chart area so that the changing length of axis labels doesn’t affect the view. The second example also shows how to implement real-time chart when generating data takes a longer time. In this case, the calculation should run on a background thread, which can be implemented by starting the asynchronous workflow in the background using Async.Start and by switching to the GUI thread once the data is available.

Additional Resources

This article is the last article in a series that discusses using Microsoft Chart Controls library directly. The previous tutorial focused on data binding and the first article introduced basic aspects of the library:

The sample in this tutorial uses the Chart Controls library directly using the types available in .NET 4.0. An alternative option is to use the FSharpChart library that builds on top of Chart Controls but adds more F#-friendly API. For more information see the following articles:

To download the code snippets shown in this article, go to https://code.msdn.microsoft.com/Chapter-6-Visualizing-Data-c68a2296

See Also

This article is based on Real World Functional Programming: With Examples in F# and C#. Book chapters related to the content of this article are:

  • Book Chapter 4: “Exploring F# and .NET libraries by example” demonstrates how to create a simple charting application from scratch. This chapter is useful for learning F# programming when focusing on working with data.

  • Book Chapter 12: “Sequence expressions and alternative workflows” explains how to work with in-memory data sets in F# using sequence expressions and higher-order functions.

  • Book Chapter 13: “Asynchronous and data-driven programming” shows how to use asynchronous workflows to obtain data from the internet, how to convert data to a structured format, and how to chart it using Excel.

The following MSDN documents are related to the topic of this article:

  • Performance Counters explains how Windows performance counters work in general and provides a list of available counters.

  • PerformanceCounter Class provides detailed documentation about the class that is used to retrieve performance counter statistics.

  • Chart Controls is a MSDN section dedicated to working with Microsoft Chart Controls with many examples in C# and Visual Basic. It includes a comprehensive reference with all chart types and properties.

  • Async.SwitchToContext Method (F#) is a method that allows us to switch from a background thread to a Windows Forms thread so that we can safely update the chart. The MSDN page contains some additional examples.

Previous article: Tutorial: Creating a Series Using Data Binding

Next article: Overview: Getting Started with the FSharpChart Library