ChartDirector 6.2 (.NET Edition)

Realtime Chart with Track Line (Windows)




NOTE: This section describes Realtime Chart with Track Line for Windows applications only. For web applications, please refer to Realtime Chart with Track Line (Web).

NOTE: For conciseness, some of the following descriptions only mention WinChartViewer. Those descriptions apply to WPFChartViewer as well.

This sample program demonstrates a realtime chart with configurable chart update rate. It includes a track cursor that updates the legend to display the data values as the mouse cursor moves over the chart. When the mouse is not over the chart, the track cursor will display the latest data values in the legend.

In this sample program, new values are generated by a random number generator driven by a timer. The values are initially appended to data arrays which are used for creating the chart. When the number of values exceeds the array size, new values will be "shifted" into the array.

The chart is updated by a second timer. This allows the chart update rate to be configurable independent of the data rate. Also, the chart can be "frozen" for easy reading, while the data can continue to update on the background.

To demonstrate the code structure for update rate control (even though for the update rate in this demo it is not necessary to have any rate control), instead of directly updating the chart, the chart update timer calls WinChartViewer.updateViewPort to trigger the WinChartViewer.ViewPortChanged event , and the chart is updated in its handler.

The track cursor drawing code is essentially the same as that in Track Line with Legend (Windows). Please refer to that example for the explanation of the code.

Source Code Listing

[Windows Forms - C# version] NetWinCharts\CSharpWinCharts\frmrealtimetrack.cs
using System;
using System.Windows.Forms;
using System.Collections;
using ChartDirector;

namespace CSharpChartExplorer
{
    public partial class FrmRealTimeTrack : Form
    {
        // The data arrays that store the visible data. The data arrays are updated in realtime. In
        // this demo, we plot the last 240 samples.
        private const int sampleSize = 240;
        private DateTime[] timeStamps = new DateTime[sampleSize];
        private double[] dataSeriesA = new double[sampleSize];
        private double[] dataSeriesB = new double[sampleSize];
        private double[] dataSeriesC = new double[sampleSize];

        // The index of the array position to which new data values are added.
        private int currentIndex = 0;

        // In this demo, we use a data generator driven by a timer to generate realtime data. The
        // nextDataTime is an internal variable used by the data generator to keep track of which
        // values to generate next.
        private DateTime nextDataTime = new DateTime(DateTime.Now.Ticks / 10000000 * 10000000);

        public FrmRealTimeTrack()
        {
            InitializeComponent();
        }

        private void FrmRealTimeTrack_Load(object sender, EventArgs e)
        {
            // Data generation rate
            dataRateTimer.Interval = 250;

            // Chart update rate, which can be different from the data generation rate.
            chartUpdateTimer.Interval = (int)samplePeriod.Value;

            // Initialize data buffer to no data.
            for (int i = 0; i < timeStamps.Length; ++i)
                timeStamps[i] = DateTime.MinValue;

            // Enable RunPB button
            runPB.Checked = true;

            // Now can start the timers for data collection and chart update
            dataRateTimer.Start();
            chartUpdateTimer.Start();
        }

        //
        // The data update routine. In this demo, it is invoked every 250ms to get new data.
        //
        private void dataRateTimer_Tick(object sender, EventArgs e)
        {
            do
            {
                //
                // In this demo, we use some formulas to generate new values. In real applications,
                // it may be replaced by some data acquisition code.
                //
                double p = nextDataTime.Ticks / 10000000.0 * 4;
                double dataA = 20 + Math.Cos(p * 2.2) * 10 + 1 / (Math.Cos(p) * Math.Cos(p) + 0.01);
                double dataB = 150 + 100 * Math.Sin(p / 27.7) * Math.Sin(p / 10.1);
                double dataC = 150 + 100 * Math.Cos(p / 6.7) * Math.Cos(p / 11.9);

                // After obtaining the new values, we need to update the data arrays.
                if (currentIndex < timeStamps.Length)
                {
                    // Store the new values in the current index position, and increment the index.
                    dataSeriesA[currentIndex] = dataA;
                    dataSeriesB[currentIndex] = dataB;
                    dataSeriesC[currentIndex] = dataC;
                    timeStamps[currentIndex] = nextDataTime;
                    ++currentIndex;
                }
                else
                {
                    // The data arrays are full. Shift the arrays and store the values at the end.
                    shiftData(dataSeriesA, dataA);
                    shiftData(dataSeriesB, dataB);
                    shiftData(dataSeriesC, dataC);
                    shiftData(timeStamps, nextDataTime);
                }

                // Update nextDataTime. This is needed by our data generator. In real applications,
                // you may not need this variable or the associated do/while loop.
                nextDataTime = nextDataTime.AddMilliseconds(dataRateTimer.Interval);
            }
            while (nextDataTime < DateTime.Now);

            // We provide some visual feedback to the numbers generated, so you can see the
            // values being generated.
            valueA.Text = dataSeriesA[currentIndex - 1].ToString(".##");
            valueB.Text = dataSeriesB[currentIndex - 1].ToString(".##");
            valueC.Text = dataSeriesC[currentIndex - 1].ToString(".##");
        }

        //
        // Utility to shift a double value into an array
        //
        private void shiftData(double[] data, double newValue)
        {
            for (int i = 1; i < data.Length; ++i)
                data[i - 1] = data[i];
            data[data.Length - 1] = newValue;
        }

        //
        // Utility to shift a DataTime value into an array
        //
        private void shiftData(DateTime[] data, DateTime newValue)
        {
            for (int i = 1; i < data.Length; ++i)
                data[i - 1] = data[i];
            data[data.Length - 1] = newValue;
        }

        //
        // Enable/disable chart update based on the state of the Run button.
        //
        private void runPB_CheckedChanged(object sender, EventArgs e)
        {
            chartUpdateTimer.Enabled = runPB.Checked;
        }

        //
        // Updates the chartUpdateTimer interval if the user selects another interval.
        //
        private void samplePeriod_ValueChanged(object sender, EventArgs e)
        {
            chartUpdateTimer.Interval = (int)samplePeriod.Value;
        }

        //
        // The chartUpdateTimer Tick event - this updates the chart periodicially by raising
        // viewPortChanged events.
        //
        private void chartUpdateTimer_Tick(object sender, EventArgs e)
        {
            winChartViewer1.updateViewPort(true, false);
        }

        //
        // The viewPortChanged event handler. In this example, it just updates the chart. If you
        // have other controls to update, you may also put the update code here.
        //
        private void winChartViewer1_ViewPortChanged(object sender, WinViewPortEventArgs e)
        {
            drawChart(winChartViewer1);
        }

        //
        // Draw the chart and display it in the given viewer.
        //
        private void drawChart(WinChartViewer viewer)
        {
            // Create an XYChart object 600 x 270 pixels in size, with light grey (f4f4f4) 
            // background, black (000000) border, 1 pixel raised effect, and with a rounded frame.
            XYChart c = new XYChart(600, 270, 0xf4f4f4, 0x000000, 1);
            c.setRoundedFrame(Chart.CColor(BackColor));

            // Set the plotarea at (55, 55) and of size 520 x 185 pixels. Use white (ffffff) 
            // background. Enable both horizontal and vertical grids by setting their colors to 
            // grey (cccccc). Set clipping mode to clip the data lines to the plot area.
            c.setPlotArea(55, 55, 520, 185, 0xffffff, -1, -1, 0xcccccc, 0xcccccc);
            c.setClipping();

            // Add a title to the chart using 15 pts Times New Roman Bold Italic font, with a light
            // grey (dddddd) background, black (000000) border, and a glass like raised effect.
            c.addTitle("Field Intensity at Observation Satellite", "Times New Roman Bold Italic", 15
                ).setBackground(0xdddddd, 0x000000, Chart.glassEffect());

            // Set the reference font size of the legend box
            c.getLegend().setFontSize(8);

            // Configure the y-axis with a 10pts Arial Bold axis title
            c.yAxis().setTitle("Intensity (V/m)", "Arial Bold", 10);

            // Configure the x-axis to auto-scale with at least 75 pixels between major tick and 15 
            // pixels between minor ticks. This shows more minor grid lines on the chart.
            c.xAxis().setTickDensity(75, 15);

            // Set the axes width to 2 pixels
            c.xAxis().setWidth(2);
            c.yAxis().setWidth(2);

            // Now we add the data to the chart
            DateTime firstTime = timeStamps[0];
            if (firstTime != DateTime.MinValue)
            {
                // Set up the x-axis scale. In this demo, we set the x-axis to show the 240 samples,
                // with 250ms per sample.
                c.xAxis().setDateScale(firstTime, firstTime.AddSeconds(
                    dataRateTimer.Interval * timeStamps.Length / 1000));

                // Set the x-axis label format
                c.xAxis().setLabelFormat("{value|hh:nn:ss}");

                // Create a line layer to plot the lines
                LineLayer layer = c.addLineLayer2();

                // The x-coordinates are the timeStamps.
                layer.setXData(timeStamps);

                // The 3 data series are used to draw 3 lines.
                layer.addDataSet(dataSeriesA, 0xff0000, "Alpha");
                layer.addDataSet(dataSeriesB, 0x00cc00, "Beta");
                layer.addDataSet(dataSeriesC, 0x0000ff, "Gamma");
            }

            // Include track line with legend. If the mouse is on the plot area, show the track 
            // line with legend at the mouse position; otherwise, show them for the latest data
            // values (that is, at the rightmost position).
            trackLineLegend(c, viewer.IsMouseOnPlotArea ? viewer.PlotAreaMouseX :
                c.getPlotArea().getRightX());

            // Assign the chart to the WinChartViewer
            viewer.Chart = c;
        }

        //
        // Draw track cursor when mouse is moving over plotarea
        //
        private void winChartViewer1_MouseMovePlotArea(object sender, MouseEventArgs e)
        {
            WinChartViewer viewer = (WinChartViewer)sender;
            trackLineLegend((XYChart)viewer.Chart, viewer.PlotAreaMouseX);
            viewer.updateDisplay();
        }

        //
        // Draw the track line with legend
        //
        private void trackLineLegend(XYChart c, int mouseX)
        {
            // Clear the current dynamic layer and get the DrawArea object to draw on it.
            DrawArea d = c.initDynamicLayer();

            // The plot area object
            PlotArea plotArea = c.getPlotArea();

            // Get the data x-value that is nearest to the mouse, and find its pixel coordinate.
            double xValue = c.getNearestXValue(mouseX);
            int xCoor = c.getXCoor(xValue);

            // Draw a vertical track line at the x-position
            d.vline(plotArea.getTopY(), plotArea.getBottomY(), xCoor, d.dashLineColor(0x000000, 0x0101));

            // Container to hold the legend entries
            ArrayList legendEntries = new ArrayList();

            // Iterate through all layers to build the legend array
            for (int i = 0; i < c.getLayerCount(); ++i) {
                Layer layer = c.getLayerByZ(i);

                // The data array index of the x-value
                int xIndex = layer.getXIndexOf(xValue);

                // Iterate through all the data sets in the layer
                for (int j = 0; j < layer.getDataSetCount(); ++j) {
                    ChartDirector.DataSet dataSet = layer.getDataSetByZ(j);

                    // We are only interested in visible data sets with names
                    string dataName = dataSet.getDataName();
                    int color = dataSet.getDataColor();
                    if ((!string.IsNullOrEmpty(dataName)) && (color != Chart.Transparent)) {
                        // Build the legend entry, consist of the legend icon, name and data value.
                        double dataValue = dataSet.getValue(xIndex);
                        legendEntries.Add("<*block*>" + dataSet.getLegendIcon() + " " + dataName + ": " + ((
                            dataValue == Chart.NoValue) ? "N/A" : c.formatValue(dataValue, "{value|P4}")) +
                            "<*/*>");

                        // Draw a track dot for data points within the plot area
                        int yCoor = c.getYCoor(dataSet.getPosition(xIndex), dataSet.getUseYAxis());
                        if ((yCoor >= plotArea.getTopY()) && (yCoor <= plotArea.getBottomY())) {
                            d.circle(xCoor, yCoor, 4, 4, color, color);
                        }
                    }
                }
            }

            // Create the legend by joining the legend entries
            legendEntries.Reverse();
            string legendText = "<*block,maxWidth=" + plotArea.getWidth() + "*><*block*><*font=Arial Bold*>["
                 + c.xAxis().getFormattedLabel(xValue, "hh:nn:ss.ff") + "]<*/*>        " + String.Join(
                "        ", (string[])legendEntries.ToArray(typeof(string))) + "<*/*>";

            // Display the legend on the top of the plot area
            TTFText t = d.text(legendText, "Arial", 8);
            t.draw(plotArea.getLeftX() + 5, plotArea.getTopY() - 3, 0x000000, Chart.BottomLeft);
        }
    }
}

[Windows Forms - VB Version] NetWinCharts\VBNetWinCharts\frmrealtimetrack.vb
Imports ChartDirector

Public Class FrmRealTimeTrack

    ' The data arrays that store the visible data. The data arrays are updated in realtime. In
    ' this demo, we plot the last 240 samples.
    Private Const sampleSize As Integer = 240
    Private dataSeriesA(sampleSize - 1) As Double
    Private dataSeriesB(sampleSize - 1) As Double
    Private dataSeriesC(sampleSize - 1) As Double
    Private timeStamps(sampleSize - 1) As Date

    ' The index of the array position to which new data values are added.
    Private currentIndex As Integer = 0

    ' In this demo, we use a data generator driven by a timer to generate realtime data. The
    ' nextDataTime is an internal variable used by the data generator to keep track of which
    ' values to generate next.
    Private nextDataTime As DateTime = New DateTime((Now.Ticks \ 10000000) * 10000000)

    Private Sub FrmZoomScrollTrack_Load(ByVal sender As Object, ByVal e As EventArgs) _
        Handles MyBase.Load

        ' Data generation rate
        dataRateTimer.Interval = 250

        ' Chart update rate
        chartUpdateTimer.Interval = CInt(samplePeriod.Value)

        ' Initialize data buffer. In this demo, we just set the initial state to no data.
        Dim i As Integer
        For i = 0 To UBound(timeStamps)
            timeStamps(i) = DateTime.MinValue
        Next

        ' Enable RunPB button
        runPB.Checked = True

        ' Now can start the timers for data collection and chart update
        dataRateTimer.Start()
        chartUpdateTimer.Start()

    End Sub

    '/ <summary>
    '/ The data generator - invoke once every 250ms
    '/ </summary>
    Private Sub dataRateTimer_Tick(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles dataRateTimer.Tick

        Do While nextDataTime < DateTime.Now
            '
            ' In this demo, we use some formulas to generate new values. In real applications,
            ' it may be replaced by some data acquisition code.
            '
            Dim p As Double = nextDataTime.Ticks / 10000000.0 * 4
            Dim dataA As Double = 20 + Math.Cos(p * 2.2) * 10 + 1 / (Math.Cos(p) * Math.Cos(p) + 0.01)
            Dim dataB As Double = 150 + 100 * Math.Sin(p / 27.7) * Math.Sin(p / 10.1)
            Dim dataC As Double = 150 + 100 * Math.Cos(p / 6.7) * Math.Cos(p / 11.9)

            ' We provide some visual feedback to the numbers generated, so you can see the
            ' values being generated.
            valueA.Text = dataA.ToString(".##")
            valueB.Text = dataB.ToString(".##")
            valueC.Text = dataC.ToString(".##")

            ' After obtaining the new values, we need to update the data arrays.
            If currentIndex < timeStamps.Length Then
                ' Store the new values in the current index position, and increment the index.
                dataSeriesA(currentIndex) = dataA
                dataSeriesB(currentIndex) = dataB
                dataSeriesC(currentIndex) = dataC
                timeStamps(currentIndex) = nextDataTime
                currentIndex += 1
            Else
                ' The data arrays are full. Shift the arrays and store the values at the end.
                shiftData(dataSeriesA, dataA)
                shiftData(dataSeriesB, dataB)
                shiftData(dataSeriesC, dataC)
                shiftData(timeStamps, nextDataTime)
            End If

            ' Update nextDataTime. This is needed by our data generator. In real applications,
            ' you may not need this variable or the associated do/while loop.
            nextDataTime = nextDataTime.AddMilliseconds(dataRateTimer.Interval)
        Loop

    End Sub

    '
    ' Utility to shift a value into an array
    '
    Private Sub shiftData(ByVal data As Object, ByVal newValue As Object)

        Dim i As Integer
        For i = 1 To UBound(data)
            data(i - 1) = data(i)
        Next
        data(UBound(data)) = newValue

    End Sub

    '
    ' Enable/disable chart update based on the state of the Run button.
    '
    Private Sub runPB_CheckedChanged(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles runPB.CheckedChanged

        chartUpdateTimer.Enabled = runPB.Checked

    End Sub

    '
    ' Updates the chartUpdateTimer interval if the user selects another interval.
    '
    Private Sub samplePeriod_ValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles samplePeriod.ValueChanged

        chartUpdateTimer.Interval = CInt(samplePeriod.Value)

    End Sub

    '
    ' The chartUpdateTimer Tick event - this updates the chart periodicially by raising
    ' viewPortChanged events.
    '
    Private Sub chartUpdateTimer_Tick(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles chartUpdateTimer.Tick

        winChartViewer1.updateViewPort(True, False)

    End Sub

    '
    ' The viewPortChanged event handler. In this example, it just updates the chart. If you
    ' have other controls to update, you may also put the update code here.
    '
    Private Sub winChartViewer1_ViewPortChanged(ByVal sender As Object, ByVal e As WinViewPortEventArgs) _
        Handles winChartViewer1.ViewPortChanged

        drawChart(winChartViewer1)

    End Sub

    '
    ' Draw the chart and display it in the given viewer.
    '
    Private Sub drawChart(ByVal viewer As WinChartViewer)

        ' Create an XYChart object 600 x 270 pixels in size, with light grey (f4f4f4) 
        ' background, black (000000) border, 1 pixel raised effect, and with a rounded frame.
        Dim c As XYChart = New XYChart(600, 270, &HF4F4F4, &H0, 1)
        c.setRoundedFrame(Chart.CColor(BackColor))

        ' Set the plotarea at (55, 55) and of size 520 x 185 pixels. Use white (ffffff) 
        ' background. Enable both horizontal and vertical grids by setting their colors to 
        ' grey (cccccc). Set clipping mode to clip the data lines to the plot area.
        c.setPlotArea(55, 55, 520, 185, &HFFFFFF, -1, -1, &HCCCCCC, &HCCCCCC)
        c.setClipping()

        ' Add a title to the chart using 15 pts Times New Roman Bold Italic font, with a light
        ' grey (dddddd) background, black (000000) border, and a glass like raised effect.
        c.addTitle("Field Intensity at Observation Satellite", "Times New Roman Bold Italic", 15 _
            ).setBackground(&HDDDDDD, &H0, Chart.glassEffect())

        ' Set the reference font size of the legend box
        c.getLegend().setFontSize(8)

        ' Configure the y-axis with a 10pts Arial Bold axis title
        c.yAxis().setTitle("Intensity (V/m)", "Arial Bold", 10)

        ' Configure the x-axis to auto-scale with at least 75 pixels between major tick and 15 
        ' pixels between minor ticks. This shows more minor grid lines on the chart.
        c.xAxis().setTickDensity(75, 15)

        ' Set the axes width to 2 pixels
        c.xAxis().setWidth(2)
        c.yAxis().setWidth(2)

        ' Now we add the data to the chart
        Dim firstTime As DateTime = timeStamps(0)
        If firstTime <> DateTime.MinValue Then
            ' Set up the x-axis scale. In this demo, we set the x-axis to show the 240 samples,
            ' with 250ms per sample.
            c.xAxis().setDateScale(firstTime, firstTime.AddSeconds( _
                dataRateTimer.Interval * timeStamps.Length / 1000))

            ' Set the x-axis label format
            c.xAxis().setLabelFormat("{value|hh:nn:ss}")

            ' Create a line layer to plot the lines
            Dim layer As LineLayer = c.addLineLayer2()

            ' The x-coordinates are the timeStamps.
            layer.setXData(timeStamps)

            ' The 3 data series are used to draw 3 lines.
            layer.addDataSet(dataSeriesA, &HFF0000, "Alpha")
            layer.addDataSet(dataSeriesB, &H00CC00, "Beta")
            layer.addDataSet(dataSeriesC, &H0000FF, "Gamma")
        End If

        ' Include track line with legend. If the mouse is on the plot area, show the track 
        ' line with legend at the mouse position; otherwise, show them for the latest data
        ' values (that is, at the rightmost position).
        trackLineLegend(c, IIf(viewer.IsMouseOnPlotArea, viewer.PlotAreaMouseX, _
            c.getPlotArea().getRightX()))

        ' Assign the chart to the WinChartViewer
        viewer.Chart = c

    End Sub

    '
    ' Draw track cursor when mouse is moving over plotarea
    '
    Private Sub winChartViewer1_MouseMovePlotArea(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.MouseEventArgs) Handles winChartViewer1.MouseMovePlotArea

        Dim viewer As WinChartViewer = sender
        trackLineLegend(viewer.Chart, viewer.PlotAreaMouseX)
        viewer.updateDisplay()

    End Sub

    '
    ' Draw the track line with legend
    '
    Private Sub trackLineLegend(ByVal c As XYChart, ByVal mouseX As Integer)

        ' Clear the current dynamic layer and get the DrawArea object to draw on it.
        Dim d As DrawArea = c.initDynamicLayer()

        ' The plot area object
        Dim plotArea As PlotArea = c.getPlotArea()

        ' Get the data x-value that is nearest to the mouse, and find its pixel coordinate.
        Dim xValue As Double = c.getNearestXValue(mouseX)
        Dim xCoor As Integer = c.getXCoor(xValue)

        ' Draw a vertical track line at the x-position
        d.vline(plotArea.getTopY(), plotArea.getBottomY(), xCoor, d.dashLineColor(&H0, &H101))

        ' Container to hold the legend entries
        Dim legendEntries As ArrayList = New ArrayList()

        ' Iterate through all layers to build the legend array
        For i As Integer = 0 To c.getLayerCount() - 1
            Dim layer As Layer = c.getLayerByZ(i)

            ' The data array index of the x-value
            Dim xIndex As Integer = layer.getXIndexOf(xValue)

            ' Iterate through all the data sets in the layer
            For j As Integer = 0 To layer.getDataSetCount() - 1
                Dim dataSet As ChartDirector.DataSet = layer.getDataSetByZ(j)

                ' We are only interested in visible data sets with names
                Dim dataName As String = dataSet.getDataName()
                Dim color As Integer = dataSet.getDataColor()
                If (Not String.IsNullOrEmpty(dataName)) And (color <> Chart.Transparent) Then
                    ' Build the legend entry, consist of the legend icon, name and data value.
                    Dim dataValue As Double = dataSet.getValue(xIndex)
                    legendEntries.Add("<*block*>" & dataSet.getLegendIcon() & " " & dataName & ": " & IIf( _
                        dataValue = Chart.NoValue, "N/A", c.formatValue(dataValue, "{value|P4}")) & "<*/*>")

                    ' Draw a track dot for data points within the plot area
                    Dim yCoor As Integer = c.getYCoor(dataSet.getPosition(xIndex), dataSet.getUseYAxis())
                    If (yCoor >= plotArea.getTopY()) And (yCoor <= plotArea.getBottomY()) Then
                        d.circle(xCoor, yCoor, 4, 4, color, color)
                    End If
                End If
            Next
        Next

        ' Create the legend by joining the legend entries
        legendEntries.Reverse()
        Dim legendText As String = "<*block,maxWidth=" & plotArea.getWidth() & _
            "*><*block*><*font=Arial Bold*>[" & c.xAxis().getFormattedLabel(xValue, "hh:nn:ss.ff") & _
            "]<*/*>        " & Join(CType(legendEntries.ToArray(GetType(String)), String()), "        ") & _
            "<*/*>"

        ' Display the legend on the top of the plot area
        Dim t As TTFText = d.text(legendText, "Arial", 8)
        t.draw(plotArea.getLeftX() + 5, plotArea.getTopY() - 3, &H0, Chart.BottomLeft)

    End Sub

End Class

[WPF - XAML] NetWPFCharts\RealTimeTrackWindow.xaml
´╗┐<Window x:Class="CSharpWPFDemo.RealTimeTrackWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CSharpWPFDemo"
        mc:Ignorable="d"
        xmlns:ChartDirector="clr-namespace:ChartDirector;assembly=netchartdir" UseLayoutRounding="True"
        Title="Realtime Chart with Track Line" SizeToContent="WidthAndHeight" ResizeMode="NoResize" Loaded="Window_Loaded"
        >
    <DockPanel>
        <Label Content="Advanced Software Engineering" Height="25" DockPanel.Dock="Top" FontFamily="Arial" FontStyle="Italic" FontWeight="Bold" 
               FontSize="13" Background="#FF02098D" Foreground="#FFF4FF04" HorizontalContentAlignment="Right"/>
        <StackPanel DockPanel.Dock="Left" Width="120" Background="#FFF0F0F0">
            <RadioButton x:Name="runPB" Style="{StaticResource {x:Type ToggleButton}}" HorizontalContentAlignment="Left" 
                         Checked="runPB_CheckedChanged" Unchecked="runPB_CheckedChanged">
                <StackPanel Orientation="Horizontal" Margin="5">
                    <Image Source="runPB.gif" Width="16" Height="16" />
                    <TextBlock Text="Run Chart" Margin="6,0,0,0" />
                </StackPanel>
            </RadioButton>
            <RadioButton x:Name="freezePB" Style="{StaticResource {x:Type ToggleButton}}" HorizontalContentAlignment="Left">
                <StackPanel Orientation="Horizontal" Margin="5">
                    <Image Source="freezePB.gif" Width="16" Height="16" />
                    <TextBlock Text="Freeze Chart" Margin="6,0,0,0" />
                </StackPanel>
            </RadioButton>
            <TextBlock Text="Updated Rate (ms)" Margin="5,30,0,0" FontWeight="Bold" />
            <ComboBox x:Name="samplePeriod" Margin="3" SelectionChanged="samplePeriod_SelectionChanged">
                <ComboBoxItem>250</ComboBoxItem>
                <ComboBoxItem>500</ComboBoxItem>
                <ComboBoxItem>750</ComboBoxItem>
                <ComboBoxItem IsSelected="True">1000</ComboBoxItem>
                <ComboBoxItem>1250</ComboBoxItem>
                <ComboBoxItem>1500</ComboBoxItem>
                <ComboBoxItem>1750</ComboBoxItem>
                <ComboBoxItem>2000</ComboBoxItem>
            </ComboBox>
            <TextBlock Text="Simulated Machine" Margin="3,30,0,1" FontWeight="Bold" />
            <Grid Margin="3">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Text="Alpha"/>
                <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Text="Beta"/>
                <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Text="Gamma"/>
                <Label x:Name="valueA" Grid.Row="0" Grid.Column="1" Content=" " Margin="6,0,0,0" Padding="2" BorderThickness="1" BorderBrush="Gray"/>
                <Label x:Name="valueB" Grid.Row="1" Grid.Column="1" Content=" " Margin="6,2,0,2" Padding="2" BorderThickness="1" BorderBrush="Gray"/>
                <Label x:Name="valueC" Grid.Row="2" Grid.Column="1" Content=" " Margin="6,0,0,0" Padding="2" BorderThickness="1" BorderBrush="Gray"/>
            </Grid>
        </StackPanel>
        <ChartDirector:WPFChartViewer x:Name="WPFChartViewer1" DockPanel.Dock="Top" Width="600" Height="270" Margin="5" ViewPortChanged="WPFChartViewer1_ViewPortChanged" MouseMovePlotArea="WPFChartViewer1_MouseMovePlotArea" />
    </DockPanel>
</Window>

[WPF - C#] NetWPFCharts\RealTimeTrackWindow.xaml.cs
´╗┐using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using System.Windows.Controls;
using ChartDirector;

namespace CSharpWPFDemo
{
    public partial class RealTimeTrackWindow : Window
    {
        // The data arrays that store the visible data. The data arrays are updated in realtime. In
        // this demo, we plot the last 240 samples.
        private const int sampleSize = 240;
        private DateTime[] timeStamps = new DateTime[sampleSize];
        private double[] dataSeriesA = new double[sampleSize];
        private double[] dataSeriesB = new double[sampleSize];
        private double[] dataSeriesC = new double[sampleSize];

        // The index of the array position to which new data values are added.
        private int currentIndex = 0;

        // In this demo, we use a data generator driven by a timer to generate realtime data. The
        // nextDataTime is an internal variable used by the data generator to keep track of which
        // values to generate next.
        private DispatcherTimer dataRateTimer = new DispatcherTimer(DispatcherPriority.Render);
        private DateTime nextDataTime = DateTime.Now;

        // Timer used to updated the chart
        private DispatcherTimer chartUpdateTimer = new DispatcherTimer(DispatcherPriority.Render);

        public RealTimeTrackWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Data generation rate = 250ms
            dataRateTimer.Interval = new TimeSpan(0, 0, 0, 0, 250);
            dataRateTimer.Tick += dataRateTimer_Tick;

            // Chart update rate, which can be different from the data generation rate.
            chartUpdateTimer.Interval = new TimeSpan(0, 0, 0, 0, int.Parse(samplePeriod.Text));
            chartUpdateTimer.Tick += chartUpdateTimer_Tick;

            // Initialize data buffer to no data.
            for (int i = 0; i < timeStamps.Length; ++i)
                timeStamps[i] = DateTime.MinValue;

            // Enable RunPB button
            runPB.IsChecked = true;

            // Now can start the timers for data collection and chart update
            dataRateTimer.Start();
            chartUpdateTimer.Start();
        }

        //
        // The data update routine. In this demo, it is invoked every 250ms to get new data.
        //
        private void dataRateTimer_Tick(object sender, EventArgs e)
        {
            do
            {
                //
                // In this demo, we use some formulas to generate new values. In real applications,
                // it may be replaced by some data acquisition code.
                //
                double p = nextDataTime.Ticks / 10000000.0 * 4;
                double dataA = 20 + Math.Cos(p * 2.2) * 10 + 1 / (Math.Cos(p) * Math.Cos(p) + 0.01);
                double dataB = 150 + 100 * Math.Sin(p / 27.7) * Math.Sin(p / 10.1);
                double dataC = 150 + 100 * Math.Cos(p / 6.7) * Math.Cos(p / 11.9);

                // After obtaining the new values, we need to update the data arrays.
                if (currentIndex < timeStamps.Length)
                {
                    // Store the new values in the current index position, and increment the index.
                    dataSeriesA[currentIndex] = dataA;
                    dataSeriesB[currentIndex] = dataB;
                    dataSeriesC[currentIndex] = dataC;
                    timeStamps[currentIndex] = nextDataTime;
                    ++currentIndex;
                }
                else
                {
                    // The data arrays are full. Shift the arrays and store the values at the end.
                    shiftData(dataSeriesA, dataA);
                    shiftData(dataSeriesB, dataB);
                    shiftData(dataSeriesC, dataC);
                    shiftData(timeStamps, nextDataTime);
                }

                // Update nextDataTime. This is needed by our data generator. In real applications,
                // you may not need this variable or the associated do/while loop.
                nextDataTime = nextDataTime.AddMilliseconds(dataRateTimer.Interval.TotalMilliseconds);
            }
            while (nextDataTime < DateTime.Now);

            // We provide some visual feedback to the numbers generated, so you can see the
            // values being generated.
            valueA.Content = dataSeriesA[currentIndex - 1].ToString(".##");
            valueB.Content = dataSeriesB[currentIndex - 1].ToString(".##");
            valueC.Content = dataSeriesC[currentIndex - 1].ToString(".##");
        }

        //
        // Utility to shift a double value into an array
        //
        private void shiftData(double[] data, double newValue)
        {
            for (int i = 1; i < data.Length; ++i)
                data[i - 1] = data[i];
            data[data.Length - 1] = newValue;
        }

        //
        // Utility to shift a DataTime value into an array
        //
        private void shiftData(DateTime[] data, DateTime newValue)
        {
            for (int i = 1; i < data.Length; ++i)
                data[i - 1] = data[i];
            data[data.Length - 1] = newValue;
        }

        //
        // Enable/disable chart update based on the state of the Run button.
        //
        private void runPB_CheckedChanged(object sender, RoutedEventArgs e)
        {
            chartUpdateTimer.IsEnabled = runPB.IsChecked == true;
        }

        //
        // Updates the chartUpdateTimer interval if the user selects another interval.
        //

        private void samplePeriod_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var selectedText = (samplePeriod.SelectedValue as ComboBoxItem).Content as string;
            if (!string.IsNullOrEmpty(selectedText))
                chartUpdateTimer.Interval = new TimeSpan(0, 0, 0, 0, int.Parse(selectedText));
        }

        //
        // The chartUpdateTimer Tick event - this updates the chart periodicially by raising
        // viewPortChanged events.
        //
        private void chartUpdateTimer_Tick(object sender, EventArgs e)
        {
            WPFChartViewer1.updateViewPort(true, false);
        }

        //
        // The viewPortChanged event handler. In this example, it just updates the chart. If you
        // have other controls to update, you may also put the update code here.
        //
        private void WPFChartViewer1_ViewPortChanged(object sender, WPFViewPortEventArgs e)
        {
            drawChart(sender as WPFChartViewer);
        }

        //
        // Draw the chart and display it in the given viewer.
        //
        private void drawChart(WPFChartViewer viewer)
        {
            // Create an XYChart object 600 x 270 pixels in size, with light grey (f4f4f4) 
            // background, black (000000) border, 1 pixel raised effect, and with a rounded frame.
            XYChart c = new XYChart(600, 270, 0xf4f4f4, 0x000000, 1);
            c.setRoundedFrame(0xffffff);

            // Set the plotarea at (55, 55) and of size 520 x 185 pixels. Use white (ffffff) 
            // background. Enable both horizontal and vertical grids by setting their colors to 
            // grey (cccccc). Set clipping mode to clip the data lines to the plot area.
            c.setPlotArea(55, 55, 520, 185, 0xffffff, -1, -1, 0xcccccc, 0xcccccc);
            c.setClipping();

            // Add a title to the chart using 15 pts Times New Roman Bold Italic font, with a light
            // grey (dddddd) background, black (000000) border, and a glass like raised effect.
            c.addTitle("Field Intensity at Observation Satellite", "Times New Roman Bold Italic", 15
                ).setBackground(0xdddddd, 0x000000, Chart.glassEffect());

            // Set the reference font size of the legend box
            c.getLegend().setFontSize(8);

            // Configure the y-axis with a 10pts Arial Bold axis title
            c.yAxis().setTitle("Intensity (V/m)", "Arial Bold", 10);

            // Configure the x-axis to auto-scale with at least 75 pixels between major tick and 15 
            // pixels between minor ticks. This shows more minor grid lines on the chart.
            c.xAxis().setTickDensity(75, 15);

            // Set the axes width to 2 pixels
            c.xAxis().setWidth(2);
            c.yAxis().setWidth(2);

            // Now we add the data to the chart
            DateTime firstTime = timeStamps[0];
            if (firstTime != DateTime.MinValue)
            {
                // Set up the x-axis scale. In this demo, we set the x-axis to show the 240 samples,
                // with 250ms per sample.
                c.xAxis().setDateScale(firstTime, firstTime.AddSeconds(
                    dataRateTimer.Interval.TotalSeconds * timeStamps.Length));

                // Set the x-axis label format
                c.xAxis().setLabelFormat("{value|hh:nn:ss}");

                // Create a line layer to plot the lines
                LineLayer layer = c.addLineLayer2();

                // The x-coordinates are the timeStamps.
                layer.setXData(timeStamps);

                // The 3 data series are used to draw 3 lines.
                layer.addDataSet(dataSeriesA, 0xff0000, "Alpha");
                layer.addDataSet(dataSeriesB, 0x00cc00, "Beta");
                layer.addDataSet(dataSeriesC, 0x0000ff, "Gamma");
            }

            // Include track line with legend. If the mouse is on the plot area, show the track 
            // line with legend at the mouse position; otherwise, show them for the latest data
            // values (that is, at the rightmost position).
            trackLineLegend(c, viewer.IsMouseOnPlotArea ? viewer.PlotAreaMouseX :
                c.getPlotArea().getRightX());

            // Assign the chart to the WinChartViewer
            viewer.Chart = c;
        }

        //
        // Draw track cursor when mouse is moving over plotarea
        //
        private void WPFChartViewer1_MouseMovePlotArea(object sender, MouseEventArgs e)
        {
            var viewer = sender as WPFChartViewer;
            trackLineLegend((XYChart)viewer.Chart, viewer.PlotAreaMouseX);
            viewer.updateDisplay();
        }

        //
        // Draw the track line with legend
        //
        private void trackLineLegend(XYChart c, int mouseX)
        {
            // Clear the current dynamic layer and get the DrawArea object to draw on it.
            DrawArea d = c.initDynamicLayer();

            // The plot area object
            PlotArea plotArea = c.getPlotArea();

            // Get the data x-value that is nearest to the mouse, and find its pixel coordinate.
            double xValue = c.getNearestXValue(mouseX);
            int xCoor = c.getXCoor(xValue);

            // Draw a vertical track line at the x-position
            d.vline(plotArea.getTopY(), plotArea.getBottomY(), xCoor, d.dashLineColor(0x000000, 0x0101));

            // Container to hold the legend entries
            var legendEntries = new List<string>();

            // Iterate through all layers to build the legend array
            for (int i = 0; i < c.getLayerCount(); ++i)
            {
                Layer layer = c.getLayerByZ(i);

                // The data array index of the x-value
                int xIndex = layer.getXIndexOf(xValue);

                // Iterate through all the data sets in the layer
                for (int j = 0; j < layer.getDataSetCount(); ++j)
                {
                    ChartDirector.DataSet dataSet = layer.getDataSetByZ(j);

                    // We are only interested in visible data sets with names
                    string dataName = dataSet.getDataName();
                    int color = dataSet.getDataColor();
                    if ((!string.IsNullOrEmpty(dataName)) && (color != Chart.Transparent))
                    {
                        // Build the legend entry, consist of the legend icon, name and data value.
                        double dataValue = dataSet.getValue(xIndex);
                        legendEntries.Add("<*block*>" + dataSet.getLegendIcon() + " " + dataName + ": " + ((
                            dataValue == Chart.NoValue) ? "N/A" : c.formatValue(dataValue, "{value|P4}")) +
                            "<*/*>");

                        // Draw a track dot for data points within the plot area
                        int yCoor = c.getYCoor(dataSet.getPosition(xIndex), dataSet.getUseYAxis());
                        if ((yCoor >= plotArea.getTopY()) && (yCoor <= plotArea.getBottomY()))
                        {
                            d.circle(xCoor, yCoor, 4, 4, color, color);
                        }
                    }
                }
            }

            // Create the legend by joining the legend entries
            legendEntries.Reverse();
            string legendText = "<*block,maxWidth=" + plotArea.getWidth() + "*><*block*><*font=Arial Bold*>["
                 + c.xAxis().getFormattedLabel(xValue, "hh:nn:ss.ff") + "]<*/*>        " + String.Join(
                "        ", legendEntries) + "<*/*>";

            // Display the legend on the top of the plot area
            TTFText t = d.text(legendText, "Arial", 8);
            t.draw(plotArea.getLeftX() + 5, plotArea.getTopY() - 3, 0x000000, Chart.BottomLeft);
        }
    }
}