ChartDirector 6.2 (.NET Edition)

Realtime Chart with Zooming and Scrolling (Windows)




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

This example demonstrates a zoomable and scrollable realtime chart with configurable chart update rate. The chart is zoomable and scrollable and include a track cursor like that in the Zooming and Scrolling with Track Line (1) (Windows) example. It can zoom and scroll by clicking and dragging on the chart, by using the mouse wheel, and by using the scroll bar. The track cursor updates the legend dynamically to display the data values as the mouse cursor moves over the chart.

The realtime data in this example are produced by a random number generator driven with a timer. The values are appended to data arrays which are used for creating the chart. If the arrays are full, the earliest 5% of the data will be removed from them to leave space for new data. The display is updated by a second timer. This allows the display update rate to be configurable independent of the data rate.

As this chart is zoomable and scrollable, when new data arrives, in addition to updating the chart, the viewport and the scrollbar would need to update to reflect the updated data range. In this example, two alternative update methods are used depending on what is currently displayed:

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\frmrealtimezoomscroll.cs
using System;
using System.Windows.Forms;
using ChartDirector;

namespace CSharpChartExplorer
{
    public partial class FrmRealTimeZoomScroll : Form
    {
        // The data arrays that store the realtime data. The data arrays are updated in realtime. 
        // In this demo, we store at most 10000 values. 
        private const int sampleSize = 10000;
        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;

        // The full range is initialized to 60 seconds of data. It can be extended when more data
        // are available.
        private int initialFullRange = 60;

        // The maximum zoom in is 10 seconds.
        private int zoomInLimit = 10;

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

        private void FrmRealtimeZoomScroll_Load(object sender, EventArgs e)
        {
            // Initialize the WinChartViewer
            initChartViewer(winChartViewer1);

            // Data generation rate
            dataRateTimer.Interval = 250;

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

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

        //
        // Initialize the WinChartViewer
        //
        private void initChartViewer(WinChartViewer viewer)
        {
            viewer.MouseWheelZoomRatio = 1.1;

            // Initially set the mouse usage to "Pointer" mode (Drag to Scroll mode)
            pointerPB.Checked = true;
        }

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

                // In this demo, if the data arrays are full, the oldest 5% of data are discarded.
                if (currentIndex >= timeStamps.Length)
                {
                    currentIndex = sampleSize * 95 / 100 - 1;

                    for (int i = 0; i < currentIndex; ++i)
                    {
                        int srcIndex = i + sampleSize - currentIndex;
                        timeStamps[i] = timeStamps[srcIndex];
                        dataSeriesA[i] = dataSeriesA[srcIndex];
                        dataSeriesB[i] = dataSeriesB[srcIndex];
                        dataSeriesC[i] = dataSeriesC[srcIndex];
                    }
                }

                // Store the new values in the current index position, and increment the index.
                timeStamps[currentIndex] = nextDataTime;
                dataSeriesA[currentIndex] = dataA;
                dataSeriesB[currentIndex] = dataB;
                dataSeriesC[currentIndex] = dataC;
                ++currentIndex; 
                
                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(".##");
        }

        //
        // Update the chart and the viewport periodically
        //
        private void chartUpdateTimer_Tick(object sender, EventArgs e)
        {
            WinChartViewer viewer = winChartViewer1;

            if (currentIndex > 0)
            {
                //
                // As we added more data, we may need to update the full range. 
                //

                DateTime startDate = timeStamps[0];
                DateTime endDate = timeStamps[currentIndex - 1];

                // Use the initialFullRange if this is sufficient.
                double duration = endDate.Subtract(startDate).TotalSeconds;
                if (duration < initialFullRange)
                    endDate = startDate.AddSeconds(initialFullRange);

                // Update the full range to reflect the actual duration of the data. In this case, 
                // if the view port is viewing the latest data, we will scroll the view port as new
                // data are added. If the view port is viewing historical data, we would keep the 
                // axis scale unchanged to keep the chart stable.
                int updateType = Chart.ScrollWithMax;
                if (viewer.ViewPortLeft + viewer.ViewPortWidth < 0.999)
                    updateType = Chart.KeepVisibleRange;
                bool axisScaleHasChanged = viewer.updateFullRangeH("x", startDate, endDate, updateType);
                
                // Set the zoom in limit as a ratio to the full range
                viewer.ZoomInWidthLimit = zoomInLimit / (viewer.getValueAtViewPort("x", 1) - 
                    viewer.getValueAtViewPort("x", 0));
                
                // Trigger the viewPortChanged event to update the display if the axis scale has 
                // changed or if new data are added to the existing axis scale.
                if (axisScaleHasChanged || (duration < initialFullRange))
                    viewer.updateViewPort(true, false);
            }
        }

        //
        // The ViewPortChanged event handler. This event occurs if the user scrolls or zooms in
        // or out the chart by dragging or clicking on the chart. It can also be triggered by
        // calling WinChartViewer.updateViewPort.
        //
        private void winChartViewer1_ViewPortChanged(object sender, WinViewPortEventArgs e)
        {
            // In addition to updating the chart, we may also need to update other controls that
            // changes based on the view port.
            updateControls(winChartViewer1);

            // Update the chart if necessary
            if (e.NeedUpdateChart)
                drawChart(winChartViewer1);
        }

        //
        // Update other controls when the view port changed
        //
        private void updateControls(WinChartViewer viewer)
        {
            // Update the scroll bar to reflect the view port position and width.           
            hScrollBar1.Enabled = winChartViewer1.ViewPortWidth < 1;
            hScrollBar1.LargeChange = (int)Math.Ceiling(winChartViewer1.ViewPortWidth *
                (hScrollBar1.Maximum - hScrollBar1.Minimum));
            hScrollBar1.SmallChange = (int)Math.Ceiling(hScrollBar1.LargeChange * 0.1);
            hScrollBar1.Value = (int)Math.Round(winChartViewer1.ViewPortLeft *
                (hScrollBar1.Maximum - hScrollBar1.Minimum)) + hScrollBar1.Minimum;
        }

        //
        // Draw the chart.
        //
        private void drawChart(WinChartViewer viewer)
        {
            // Get the start date and end date that are visible on the chart.
            DateTime viewPortStartDate = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft));
            DateTime viewPortEndDate = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft +
                viewer.ViewPortWidth));

            // Extract the part of the data arrays that are visible.
            DateTime[] viewPortTimeStamps = null;
            double[] viewPortDataSeriesA = null;
            double[] viewPortDataSeriesB = null;
            double[] viewPortDataSeriesC = null;

            if (currentIndex > 0)
            {
                // Get the array indexes that corresponds to the visible start and end dates
                int startIndex = (int)Math.Floor(Chart.bSearch2(timeStamps, 0, currentIndex, viewPortStartDate));
                int endIndex = (int)Math.Ceiling(Chart.bSearch2(timeStamps, 0, currentIndex, viewPortEndDate));
                int noOfPoints = endIndex - startIndex + 1;
                
                // Extract the visible data
                viewPortTimeStamps = (DateTime[])Chart.arraySlice(timeStamps, startIndex, noOfPoints);
                viewPortDataSeriesA = (double[])Chart.arraySlice(dataSeriesA, startIndex, noOfPoints);
                viewPortDataSeriesB = (double[])Chart.arraySlice(dataSeriesB, startIndex, noOfPoints);
                viewPortDataSeriesC = (double[])Chart.arraySlice(dataSeriesC, startIndex, noOfPoints);
            }

            //
            // At this stage, we have extracted the visible data. We can use those data to plot the chart.
            //

            //================================================================================
            // Configure overall chart appearance.
            //================================================================================

            // Create an XYChart object of size 640 x 350 pixels
            XYChart c = new XYChart(640, 350);

            // Set the plotarea at (55, 50) with width 80 pixels less than chart width, and height 85 pixels
            // less than chart height. Use a vertical gradient from light blue (f0f6ff) to sky blue (a0c0ff)
            // as background. Set border to transparent and grid lines to white (ffffff).
            c.setPlotArea(55, 50, c.getWidth() - 85, c.getHeight() - 80, c.linearGradientColor(0, 50, 0,
                c.getHeight() - 35, 0xf0f6ff, 0xa0c0ff), -1, Chart.Transparent, 0xffffff, 0xffffff);

            // As the data can lie outside the plotarea in a zoomed chart, we need enable clipping.
            c.setClipping();

            // Add a title to the chart using 18 pts Times New Roman Bold Italic font
            c.addTitle("  Realtime Chart with Zoom/Scroll and Track Line", "Arial", 18);

            // Add a legend box at (55, 25) using horizontal layout. Use 8pts Arial Bold as font. Set the
            // background and border color to Transparent and use line style legend key.
            LegendBox b = c.addLegend(55, 25, false, "Arial Bold", 10);
            b.setBackground(Chart.Transparent);
            b.setLineStyleKey();

            // Set the x and y axis stems to transparent and the label font to 10pt Arial
            c.xAxis().setColors(Chart.Transparent);
            c.yAxis().setColors(Chart.Transparent);
            c.xAxis().setLabelStyle("Arial", 10);
            c.yAxis().setLabelStyle("Arial", 10);

            // Add axis title using 10pts Arial Bold Italic font
            c.yAxis().setTitle("Ionic Temperature (C)", "Arial Bold", 10);

            //================================================================================
            // Add data to chart
            //================================================================================

            //
            // In this example, we represent the data by lines. You may modify the code below to use other
            // representations (areas, scatter plot, etc).
            //

            // Add a line layer for the lines, using a line width of 2 pixels
            LineLayer layer = c.addLineLayer2();
            layer.setLineWidth(2);
            layer.setFastLineMode();

            // Now we add the 3 data series to a line layer, using the color red (ff0000), green (00cc00)
            // and blue (0000ff)
            layer.setXData(viewPortTimeStamps);
            layer.addDataSet(viewPortDataSeriesA, 0xff0000, "Alpha");
            layer.addDataSet(viewPortDataSeriesB, 0x00cc00, "Beta");
            layer.addDataSet(viewPortDataSeriesC, 0x0000ff, "Gamma");

            //================================================================================
            // Configure axis scale and labelling
            //================================================================================

            if (currentIndex > 0)
                c.xAxis().setDateScale(viewPortStartDate, viewPortEndDate);

            // For the automatic axis labels, set the minimum spacing to 75/30 pixels for the x/y axis.
            c.xAxis().setTickDensity(75);
            c.yAxis().setTickDensity(30);

            //
            // In a zoomable chart, the time range can be from a few years to a few seconds. We can need
            // to define the date/time format the various cases. 
            //

            // If all ticks are year aligned, we use "yyyy" as the label format.
            c.xAxis().setFormatCondition("align", 360 * 86400);
            c.xAxis().setLabelFormat("{value|yyyy}");

            // If all ticks are month aligned, we use "mmm yyyy" in bold font as the first label of a year,
            // and "mmm" for other labels.
            c.xAxis().setFormatCondition("align", 30 * 86400);
            c.xAxis().setMultiFormat(Chart.StartOfYearFilter(), "<*font=bold*>{value|mmm yyyy}",
                Chart.AllPassFilter(), "{value|mmm}");

            // If all ticks are day algined, we use "mmm dd<*br*>yyyy" in bold font as the first label of a
            // year, and "mmm dd" in bold font as the first label of a month, and "dd" for other labels.
            c.xAxis().setFormatCondition("align", 86400);
            c.xAxis().setMultiFormat(Chart.StartOfYearFilter(),
                "<*block,halign=left*><*font=bold*>{value|mmm dd<*br*>yyyy}", Chart.StartOfMonthFilter(),
                "<*font=bold*>{value|mmm dd}");
            c.xAxis().setMultiFormat2(Chart.AllPassFilter(), "{value|dd}");

            // If all ticks are hour algined, we use "hh:nn<*br*>mmm dd" in bold font as the first label of 
            // the Day, and "hh:nn" for other labels.
            c.xAxis().setFormatCondition("align", 3600);
            c.xAxis().setMultiFormat(Chart.StartOfDayFilter(), "<*font=bold*>{value|hh:nn<*br*>mmm dd}",
                Chart.AllPassFilter(), "{value|hh:nn}");

            // If all ticks are minute algined, then we use "hh:nn" as the label format.
            c.xAxis().setFormatCondition("align", 60);
            c.xAxis().setLabelFormat("{value|hh:nn}");

            // If all other cases, we use "hh:nn:ss" as the label format.
            c.xAxis().setFormatCondition("else");
            c.xAxis().setLabelFormat("{value|hh:nn:ss}");

            // We make sure the tick increment must be at least 1 second.
            c.xAxis().setMinTickInc(1);

            //================================================================================
            // Output the chart
            //================================================================================

            // We need to update the track line too. If the mouse is moving on the chart (eg. if 
            // the user drags the mouse on the chart to scroll it), the track line will be updated
            // in the MouseMovePlotArea event. Otherwise, we need to update the track line here.
            if (!winChartViewer1.IsInMouseMoveEvent)
            {
                trackLineLabel(c, (null == viewer.Chart) ? c.getPlotArea().getRightX() : 
                    viewer.PlotAreaMouseX);
            }
                        
            viewer.Chart = c;
        }

        //
        // Draw track line with data labels
        //
        private void trackLineLabel(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);
            if (xCoor < plotArea.getLeftX())
                return;

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

            // Draw a label on the x-axis to show the track line position.
            string xlabel = "<*font,bgColor=000000*> " + c.xAxis().getFormattedLabel(xValue, "hh:nn:ss.ff") +
                " <*/font*>";
            TTFText t = d.text(xlabel, "Arial Bold", 10);

            // Restrict the x-pixel position of the label to make sure it stays inside the chart image.
            int xLabelPos = Math.Max(0, Math.Min(xCoor - t.getWidth() / 2, c.getWidth() - t.getWidth()));
            t.draw(xLabelPos, plotArea.getBottomY() + 6, 0xffffff);

            // Iterate through all layers to draw the data labels
            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);

                    // Get the color and position of the data label
                    int color = dataSet.getDataColor();
                    int yCoor = c.getYCoor(dataSet.getPosition(xIndex), dataSet.getUseYAxis());

                    // Draw a track dot with a label next to it for visible data points in the plot area
                    if ((yCoor >= plotArea.getTopY()) && (yCoor <= plotArea.getBottomY()) && (color !=
                        Chart.Transparent) && (!string.IsNullOrEmpty(dataSet.getDataName())))
                    {
                        d.circle(xCoor, yCoor, 4, 4, color, color);

                        string label = "<*font,bgColor=" + color.ToString("x") + "*> " + c.formatValue(
                            dataSet.getValue(xIndex), "{value|P4}") + " <*/font*>";
                        t = d.text(label, "Arial Bold", 10);

                        // Draw the label on the right side of the dot if the mouse is on the left side the
                        // chart, and vice versa. This ensures the label will not go outside the chart image.
                        if (xCoor <= (plotArea.getLeftX() + plotArea.getRightX()) / 2)
                            t.draw(xCoor + 5, yCoor, 0xffffff, Chart.Left);
                        else
                            t.draw(xCoor - 5, yCoor, 0xffffff, Chart.Right);
                    }
                }
            }
        }

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

        //
        // The scroll bar event handler
        //
        private void hScrollBar1_ValueChanged(object sender, EventArgs e)
        {
            // When the view port is changed (user drags on the chart to scroll), the scroll bar will get
            // updated. When the scroll bar changes (eg. user drags on the scroll bar), the view port will
            // get updated. This creates an infinite loop. To avoid this, the scroll bar can update the 
            // view port only if the view port is not updating the scroll bar.
            if (!winChartViewer1.IsInViewPortChangedEvent)
            {
                winChartViewer1.ViewPortLeft = ((double)(hScrollBar1.Value - hScrollBar1.Minimum))
                    / (hScrollBar1.Maximum - hScrollBar1.Minimum);

                // Trigger a view port changed event to update the chart
                winChartViewer1.updateViewPort(true, false);
            }
        }

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

        //
        // Pointer (Drag to Scroll) button event handler
        //
        private void pointerPB_CheckedChanged(object sender, EventArgs e)
        {
            if (((RadioButton)sender).Checked)
                winChartViewer1.MouseUsage = WinChartMouseUsage.ScrollOnDrag;
        }

        //
        // Zoom In button event handler
        //
        private void zoomInPB_CheckedChanged(object sender, EventArgs e)
        {
            if (((RadioButton)sender).Checked)
                winChartViewer1.MouseUsage = WinChartMouseUsage.ZoomIn;
        }

        //
        // Zoom Out button event handler
        //
        private void zoomOutPB_CheckedChanged(object sender, EventArgs e)
        {
            if (((RadioButton)sender).Checked)
                winChartViewer1.MouseUsage = WinChartMouseUsage.ZoomOut;
        }

        //
        // Save button event handler
        //
        private void savePB_Click(object sender, EventArgs e)
        {
            // The standard Save File dialog
            SaveFileDialog fileDlg = new SaveFileDialog();
            fileDlg.Filter = "PNG (*.png)|*.png|JPG (*.jpg)|*.jpg|GIF (*.gif)|*.gif|BMP (*.bmp)|*.bmp|" +
                "SVG (*.svg)|*.svg|PDF (*.pdf)|*.pdf";
            fileDlg.FileName = "chartdirector_demo";
            if (fileDlg.ShowDialog() != DialogResult.OK)
                return;

            // Save the chart
            if (null != winChartViewer1.Chart)
                winChartViewer1.Chart.makeChart(fileDlg.FileName);
        }
    }
}

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

Public Class FrmRealTimeZoomScroll

    ' 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 = 10000
    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

    ' The full range is initialized to 60 seconds of data. It can be extended when more data
    ' are available.
    Private initialFullRange As Integer = 60

    ' The maximum zoom in is 10 seconds.
    Private zoomInLimit As Integer = 10

    ' 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)

    ' Flag to indicated if initialization has been completed. Prevents events from firing before 
    ' controls are properly initialized.
    Private hasFinishedInitialization As Boolean

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

        ' Initialize the WinChartViewer
        initChartViewer(winChartViewer1)

        ' Data generation rate
        dataRateTimer.Interval = 250

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

        ' Can handle events now
        hasFinishedInitialization = True

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

    End Sub

    '
    ' Initialize the WinChartViewer
    '
    Private Sub initChartViewer(ByVal viewer As WinChartViewer)

        viewer.MouseWheelZoomRatio = 1.1

        ' Initially set the mouse usage to "Pointer" mode (Drag to Scroll mode)
        pointerPB.Checked = True

    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)

            ' In this demo, if the data arrays are full, the oldest 5% of data are discarded.
            If currentIndex >= timeStamps.Length Then
                currentIndex = Int(sampleSize * 95 / 100) - 1
                For i As Integer = 0 To currentIndex - 1
                    Dim srcIndex As Integer = i + sampleSize - currentIndex
                    timeStamps(i) = timeStamps(srcIndex)
                    dataSeriesA(i) = dataSeriesA(srcIndex)
                    dataSeriesB(i) = dataSeriesB(srcIndex)
                    dataSeriesC(i) = dataSeriesC(srcIndex)
                Next
            End If

            ' Store the new values in the current index position, and increment the index.
            timeStamps(currentIndex) = nextDataTime
            dataSeriesA(currentIndex) = dataA
            dataSeriesB(currentIndex) = dataB
            dataSeriesC(currentIndex) = dataC
            currentIndex += 1

            ' 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

        ' 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(".##")

    End Sub

    '
    ' Update the chart and the viewport periodically
    '
    Private Sub chartUpdateTimer_Tick(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles chartUpdateTimer.Tick

        Dim viewer As WinChartViewer = winChartViewer1

        If currentIndex > 0 Then
            '
            ' As we added more data, we may need to update the full range. 
            '

            Dim startDate As DateTime = timeStamps(0)
            Dim endDate As DateTime = timeStamps(currentIndex - 1)

            ' Use the initialFullRange if this is sufficient.
            Dim duration As Double = endDate.Subtract(startDate).TotalSeconds
            If duration < initialFullRange Then
                endDate = startDate.AddSeconds(initialFullRange)
            End If

            ' Update the full range to reflect the actual duration of the data. In this case, 
            ' if the view port is viewing the latest data, we will scroll the view port as new
            ' data are added. If the view port is viewing historical data, we would keep the 
            ' axis scale unchanged to keep the chart stable.
            Dim updateType As Integer = Chart.ScrollWithMax
            If viewer.ViewPortLeft + viewer.ViewPortWidth < 0.999 Then
                updateType = Chart.KeepVisibleRange
            End If
            Dim axisScaleHasChanged As Boolean = viewer.updateFullRangeH("x", startDate, endDate, updateType)

            ' Set the zoom in limit as a ratio to the full range
            viewer.ZoomInWidthLimit = zoomInLimit / (viewer.getValueAtViewPort("x", 1) - _
                viewer.getValueAtViewPort("x", 0))

            ' Trigger the viewPortChanged event to update the display if the axis scale has 
            ' changed or if new data are added to the existing axis scale.
            If axisScaleHasChanged Or duration < initialFullRange Then
                viewer.updateViewPort(True, False)
            End If

        End If

    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

        ' In addition to updating the chart, we may also need to update other controls that
        ' changes based on the view port.
        updateControls(winChartViewer1)

        ' Update the chart if necessary
        If e.NeedUpdateChart Then
            drawChart(winChartViewer1)
        End If

    End Sub

    '
    ' Update other controls when the view port changed
    '
    Private Sub updateControls(ByVal viewer As WinChartViewer)
        ' Update the scroll bar to reflect the view port position and width.           
        hScrollBar1.Enabled = winChartViewer1.ViewPortWidth < 1
        hScrollBar1.LargeChange = Math.Ceiling(winChartViewer1.ViewPortWidth * _
            (hScrollBar1.Maximum - hScrollBar1.Minimum))
        hScrollBar1.SmallChange = Math.Ceiling(hScrollBar1.LargeChange * 0.1)
        hScrollBar1.Value = Math.Round(winChartViewer1.ViewPortLeft * _
            (hScrollBar1.Maximum - hScrollBar1.Minimum)) + hScrollBar1.Minimum
    End Sub

    '
    ' Draw the chart
    '
    Private Sub drawChart(ByVal viewer As WinChartViewer)

        ' Get the start date and end date that are visible on the chart.
        Dim viewPortStartDate As DateTime = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft))
        Dim viewPortEndDate As DateTime = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft + _
            viewer.ViewPortWidth))

        ' Extract the part of the data arrays that are visible.
        Dim viewPortTimeStamps() As DateTime = Nothing
        Dim viewPortDataSeriesA() As Double = Nothing
        Dim viewPortDataSeriesB() As Double = Nothing
        Dim viewPortDataSeriesC() As Double = Nothing

        If currentIndex > 0 Then
            ' Get the array indexes that corresponds to the visible start and end dates
            Dim startIndex As Integer = Math.Floor(Chart.bSearch2(timeStamps, 0, currentIndex, viewPortStartDate))
            Dim endIndex As Integer = Math.Ceiling(Chart.bSearch2(timeStamps, 0, currentIndex, viewPortEndDate))
            Dim noOfPoints As Integer = endIndex - startIndex + 1

            ' Extract the visible data
            viewPortTimeStamps = Chart.arraySlice(timeStamps, startIndex, noOfPoints)
            viewPortDataSeriesA = Chart.arraySlice(dataSeriesA, startIndex, noOfPoints)
            viewPortDataSeriesB = Chart.arraySlice(dataSeriesB, startIndex, noOfPoints)
            viewPortDataSeriesC = Chart.arraySlice(dataSeriesC, startIndex, noOfPoints)
        End If

        '
        ' At this stage, we have extracted the visible data. We can use those data to plot the chart.
        '

        '================================================================================
        ' Configure overall chart appearance.
        '================================================================================

        ' Create an XYChart object of size 640 x 350 pixels
        Dim c As XYChart = New XYChart(640, 350)

        ' Set the plotarea at (55, 50) with width 80 pixels less than chart width, and height 85 pixels
        ' less than chart height. Use a vertical gradient from light blue (f0f6ff) to sky blue (a0c0ff)
        ' as background. Set border to transparent and grid lines to white (ffffff).
        c.setPlotArea(55, 50, c.getWidth() - 85, c.getHeight() - 80, c.linearGradientColor(0, 50, 0, _
            c.getHeight() - 35, &HF0F6FF, &HA0C0FF), -1, Chart.Transparent, &HFFFFFF, &HFFFFFF)

        ' As the data can lie outside the plotarea in a zoomed chart, we need enable clipping.
        c.setClipping()

        ' Add a title to the chart using 18 pts Times New Roman Bold Italic font
        c.addTitle("  Realtime Chart with Zoom/Scroll and Track Line", "Arial", 18)

        ' Add a legend box at (55, 25) using horizontal layout. Use 8pts Arial Bold as font. Set the
        ' background and border color to Transparent and use line style legend key.
        Dim b As LegendBox = c.addLegend(55, 25, False, "Arial Bold", 10)
        b.setBackground(Chart.Transparent)
        b.setLineStyleKey()

        ' Set the x and y axis stems to transparent and the label font to 10pt Arial
        c.xAxis().setColors(Chart.Transparent)
        c.yAxis().setColors(Chart.Transparent)
        c.xAxis().setLabelStyle("Arial", 10)
        c.yAxis().setLabelStyle("Arial", 10)

        ' Add axis title using 10pts Arial Bold Italic font
        c.yAxis().setTitle("Ionic Temperature (C)", "Arial Bold", 10)

        '================================================================================
        ' Add data to chart
        '================================================================================

        '
        ' In this example, we represent the data by lines. You may modify the code below to use other
        ' representations (areas, scatter plot, etc).
        '

        ' Add a line layer for the lines, using a line width of 2 pixels
        Dim layer As LineLayer = c.addLineLayer2()
        layer.setLineWidth(2)
        layer.setFastLineMode()

        ' Now we add the 3 data series to a line layer, using the color red (ff0000), green (00cc00)
        ' and blue (0000ff)
        layer.setXData(viewPortTimeStamps)
        layer.addDataSet(viewPortDataSeriesA, &HFF0000, "Alpha")
        layer.addDataSet(viewPortDataSeriesB, &HCC00, "Beta")
        layer.addDataSet(viewPortDataSeriesC, &HFF, "Gamma")

        '================================================================================
        ' Configure axis scale and labelling
        '================================================================================

        If currentIndex > 0 Then
            c.xAxis().setDateScale(viewPortStartDate, viewPortEndDate)
        End If

        ' For the automatic axis labels, set the minimum spacing to 75/30 pixels for the x/y axis.
        c.xAxis().setTickDensity(75)
        c.yAxis().setTickDensity(30)

        '
        ' In a zoomable chart, the time range can be from a few years to a few seconds. We can need
        ' to define the date/time format the various cases. 
        '

        ' If all ticks are year aligned, we use "yyyy" as the label format.
        c.xAxis().setFormatCondition("align", 360 * 86400)
        c.xAxis().setLabelFormat("{value|yyyy}")

        ' If all ticks are month aligned, we use "mmm yyyy" in bold font as the first label of a year,
        ' and "mmm" for other labels.
        c.xAxis().setFormatCondition("align", 30 * 86400)
        c.xAxis().setMultiFormat(Chart.StartOfYearFilter(), "<*font=bold*>{value|mmm yyyy}", _
            Chart.AllPassFilter(), "{value|mmm}")

        ' If all ticks are day algined, we use "mmm dd<*br*>yyyy" in bold font as the first label of a
        ' year, and "mmm dd" in bold font as the first label of a month, and "dd" for other labels.
        c.xAxis().setFormatCondition("align", 86400)
        c.xAxis().setMultiFormat(Chart.StartOfYearFilter(), _
            "<*block,halign=left*><*font=bold*>{value|mmm dd<*br*>yyyy}", Chart.StartOfMonthFilter(), _
            "<*font=bold*>{value|mmm dd}")
        c.xAxis().setMultiFormat2(Chart.AllPassFilter(), "{value|dd}")

        ' If all ticks are hour algined, we use "hh:nn<*br*>mmm dd" in bold font as the first label of 
        ' the Day, and "hh:nn" for other labels.
        c.xAxis().setFormatCondition("align", 3600)
        c.xAxis().setMultiFormat(Chart.StartOfDayFilter(), "<*font=bold*>{value|hh:nn<*br*>mmm dd}", _
            Chart.AllPassFilter(), "{value|hh:nn}")

        ' If all ticks are minute algined, then we use "hh:nn" as the label format.
        c.xAxis().setFormatCondition("align", 60)
        c.xAxis().setLabelFormat("{value|hh:nn}")

        ' If all other cases, we use "hh:nn:ss" as the label format.
        c.xAxis().setFormatCondition("else")
        c.xAxis().setLabelFormat("{value|hh:nn:ss}")

        ' We make sure the tick increment must be at least 1 second.
        c.xAxis().setMinTickInc(1)

        '================================================================================
        ' Output the chart
        '================================================================================

        ' We need to update the track line too. If the mouse is moving on the chart (eg. if 
        ' the user drags the mouse on the chart to scroll it), the track line will be updated
        ' in the MouseMovePlotArea event. Otherwise, we need to update the track line here.
        If Not viewer.IsInMouseMoveEvent Then
            trackLineLabel(c, IIf(IsNothing(viewer.Chart), c.getPlotArea().getRightX(), _
                viewer.PlotAreaMouseX))
        End If

        viewer.Chart = c

    End Sub

    '
    ' Draw track line with data labels
    '
    Private Sub trackLineLabel(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)
        If xCoor < plotArea.getLeftX() Then
            Return
        End If

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

        ' Draw a label on the x-axis to show the track line position.
        Dim xlabel As String = "<*font,bgColor=000000*> " & c.xAxis().getFormattedLabel(xValue, _
            "hh:nn:ss.ff") & " <*/font*>"
        Dim t As TTFText = d.text(xlabel, "Arial Bold", 10)

        ' Restrict the x-pixel position of the label to make sure it stays inside the chart image.
        Dim xLabelPos As Integer = Math.Max(0, Math.Min(xCoor - t.getWidth() / 2, _
            c.getWidth() - t.getWidth()))
        t.draw(xLabelPos, plotArea.getBottomY() + 6, &HFFFFFF)

        ' Iterate through all layers to draw the data labels
        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)

                ' Get the color and position of the data label
                Dim color As Integer = dataSet.getDataColor()
                Dim yCoor As Integer = c.getYCoor(dataSet.getPosition(xIndex), dataSet.getUseYAxis())

                ' Draw a track dot with a label next to it for visible data points in the plot area
                If (yCoor >= plotArea.getTopY()) And (yCoor <= plotArea.getBottomY()) And (color <> _
                    Chart.Transparent) Then

                    d.circle(xCoor, yCoor, 4, 4, color, color)

                    Dim label As String = "<*font,bgColor=" & Hex(color) & "*> " & c.formatValue( _
                        dataSet.getValue(xIndex), "{value|P4}") & " <*/font*>"
                    t = d.text(label, "Arial Bold", 10)

                    ' Draw the label on the right side of the dot if the mouse is on the left side the chart,
                    ' and vice versa. This ensures the label will not go outside the chart image.
                    If xCoor <= (plotArea.getLeftX() + plotArea.getRightX()) / 2 Then
                        t.draw(xCoor + 5, yCoor, &HFFFFFF, Chart.Left)
                    Else
                        t.draw(xCoor - 5, yCoor, &HFFFFFF, Chart.Right)
                    End If
                End If
            Next
        Next

    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 scroll bar event handler
    '
    Private Sub hScrollBar1_ValueChanged(ByVal sender As Object, ByVal e As EventArgs) _
        Handles hScrollBar1.ValueChanged

        ' When the view port is changed (user drags on the chart to scroll), the scroll bar will get
        ' updated. When the scroll bar changes (eg. user drags on the scroll bar), the view port will
        ' get updated. This creates an infinite loop. To avoid this, the scroll bar can update the 
        ' view port only if the view port is not updating the scroll bar.
        If hasFinishedInitialization And Not winChartViewer1.IsInViewPortChangedEvent Then
            ' Set the view port based on the scroll bar
            winChartViewer1.ViewPortLeft = (hScrollBar1.Value - hScrollBar1.Minimum) / _
                (hScrollBar1.Maximum - hScrollBar1.Minimum)

            ' Trigger a view port changed event to update the chart
            winChartViewer1.updateViewPort(True, False)
        End If

    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
        trackLineLabel(viewer.Chart, viewer.PlotAreaMouseX)
        viewer.updateDisplay()

    End Sub

    '
    ' Pointer (Drag to Scroll) button event handler
    '
    Private Sub pointerPB_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) _
        Handles pointerPB.CheckedChanged

        If sender.Checked Then
            winChartViewer1.MouseUsage = WinChartMouseUsage.ScrollOnDrag
        End If

    End Sub

    '
    ' Zoom In button event handler
    '
    Private Sub zoomInPB_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) _
        Handles zoomInPB.CheckedChanged

        If sender.Checked Then
            winChartViewer1.MouseUsage = WinChartMouseUsage.ZoomIn
        End If

    End Sub

    '
    ' Zoom Out button event handler
    '
    Private Sub zoomOutPB_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) _
        Handles zoomOutPB.CheckedChanged

        If sender.Checked Then
            winChartViewer1.MouseUsage = WinChartMouseUsage.ZoomOut
        End If

    End Sub

    '
    ' Save button event handler
    '
    Private Sub savePB_Click(ByVal sender As Object, ByVal e As EventArgs) _
        Handles savePB.Click

        ' The standard Save File dialog
        Dim fileDlg As SaveFileDialog = New SaveFileDialog()
        fileDlg.Filter = "PNG (*.png)|*.png|JPG (*.jpg)|*.jpg|GIF (*.gif)|*.gif|BMP (*.bmp)|*.bmp|" & _
            "SVG (*.svg)|*.svg|PDF (*.pdf)|*.pdf"
        fileDlg.FileName = "chartdirector_demo"
        If fileDlg.ShowDialog() <> DialogResult.OK Then
            Return
        End If
        ' Save the chart
        If Not IsNothing(winChartViewer1.Chart) Then
            winChartViewer1.Chart.makeChart(fileDlg.FileName)
        End If

    End Sub

End Class

[WPF - XAML] NetWPFCharts\RealTimeZoomScrollWindow.xaml
´╗┐<Window x:Class="CSharpWPFDemo.RealTimeZoomScrollWindow"
        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 Zoom/Scroll and 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="pointerPB" Style="{StaticResource {x:Type ToggleButton}}" HorizontalContentAlignment="Left" Checked="pointerPB_Checked" >
                <StackPanel Orientation="Horizontal" Margin="5">
                    <Image Source="pointer.gif" Height="16" />
                    <TextBlock Text="Pointer" Margin="6,0,0,0" />
                </StackPanel>
            </RadioButton>
            <RadioButton x:Name="zoomInPB" Style="{StaticResource {x:Type ToggleButton}}" HorizontalContentAlignment="Left" Checked="zoomInPB_Checked" >
                <StackPanel Orientation="Horizontal" Margin="5" >
                    <Image Source="zoomInIcon.gif" Height="16" />
                    <TextBlock Text="Zoom In" Margin="6,0,0,0" />
                </StackPanel>
            </RadioButton>
            <RadioButton x:Name="zoomOutPB" Style="{StaticResource {x:Type ToggleButton}}" HorizontalContentAlignment="Left" Checked="zoomOutPB_Checked">
                <StackPanel Orientation="Horizontal" Margin="5" >
                    <Image Source="zoomOutIcon.gif" Height="16" />
                    <TextBlock Text="Zoom Out" Margin="6,0,0,0" />
                </StackPanel>
            </RadioButton>
            <Button x:Name="savePB" Margin="0,32,0,0" HorizontalContentAlignment="Left" Click="savePB_Click">
                <StackPanel Orientation="Horizontal" Margin="5" >
                    <Image Source="saveIcon.gif" Width="14" Height="14" />
                    <TextBlock Text="Save" Margin="6,0,0,0" />
                </StackPanel>
            </Button>
            <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="640" Height="350" Margin="5" ViewPortChanged="WPFChartViewer1_ViewPortChanged" MouseMovePlotArea="WPFChartViewer1_MouseMovePlotArea" />
        <ScrollBar x:Name="hScrollBar1" DockPanel.Dock="Top" Orientation="Horizontal" ValueChanged="hScrollBar1_ValueChanged"/>
    </DockPanel>
</Window>

[WPF - C#] NetWPFCharts\RealTimeZoomScrollWindow.xaml.cs
´╗┐using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using System.Windows.Controls;
using Microsoft.Win32;
using ChartDirector;

namespace CSharpWPFDemo
{
    /// <summary>
    /// Interaction logic for RealTimeZoomScroll.xaml
    /// </summary>
    public partial class RealTimeZoomScrollWindow : Window
    {
        // The data arrays that store the realtime data. The data arrays are updated in realtime. 
        // In this demo, we store at most 10000 values. 
        private const int sampleSize = 10000;
        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;

        // The full range is initialized to 60 seconds of data. It can be extended when more data
        // are available.
        private int initialFullRange = 60;

        // The maximum zoom in is 10 seconds.
        private int zoomInLimit = 10;

        // 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 = new DateTime(DateTime.Now.Ticks / 10000000 * 10000000);

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


        public RealTimeZoomScrollWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Initialize the WinChartViewer
            initChartViewer(WPFChartViewer1);

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

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

        //
        // Initialize the WinChartViewer
        //
        private void initChartViewer(WPFChartViewer viewer)
        {
            // Enable mouse wheel zooming
            viewer.MouseWheelZoomRatio = 1.1;

            // Initially set the mouse usage to "Pointer" mode (Drag to Scroll mode)
            pointerPB.IsChecked = true;
        }

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

                // In this demo, if the data arrays are full, the oldest 5% of data are discarded.
                if (currentIndex >= timeStamps.Length)
                {
                    currentIndex = sampleSize * 95 / 100 - 1;

                    for (int i = 0; i < currentIndex; ++i)
                    {
                        int srcIndex = i + sampleSize - currentIndex;
                        timeStamps[i] = timeStamps[srcIndex];
                        dataSeriesA[i] = dataSeriesA[srcIndex];
                        dataSeriesB[i] = dataSeriesB[srcIndex];
                        dataSeriesC[i] = dataSeriesC[srcIndex];
                    }
                }

                // Store the new values in the current index position, and increment the index.
                timeStamps[currentIndex] = nextDataTime;
                dataSeriesA[currentIndex] = dataA;
                dataSeriesB[currentIndex] = dataB;
                dataSeriesC[currentIndex] = dataC;
                ++currentIndex;

                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(".##");
        }

        //
        // Update the chart and the viewport periodically
        //
        private void chartUpdateTimer_Tick(object sender, EventArgs e)
        {
            var viewer = WPFChartViewer1;

            if (currentIndex >= 0)
            {
                //
                // As we added more data, we may need to update the full range. 
                //

                DateTime startDate = timeStamps[0];
                DateTime endDate = timeStamps[currentIndex - 1];

                // Use the initialFullRange if this is sufficient.
                double duration = endDate.Subtract(startDate).TotalSeconds;
                if (duration < initialFullRange)
                    endDate = startDate.AddSeconds(initialFullRange);

                // Update the full range to reflect the actual duration of the data. In this case, 
                // if the view port is viewing the latest data, we will scroll the view port as new
                // data are added. If the view port is viewing historical data, we would keep the 
                // axis scale unchanged to keep the chart stable.
                int updateType = Chart.ScrollWithMax;
                if (viewer.ViewPortLeft + viewer.ViewPortWidth < 0.999)
                    updateType = Chart.KeepVisibleRange;
                bool axisScaleHasChanged = viewer.updateFullRangeH("x", startDate, endDate, updateType);

                // Set the zoom in limit as a ratio to the full range
                viewer.ZoomInWidthLimit = zoomInLimit / (viewer.getValueAtViewPort("x", 1) -
                    viewer.getValueAtViewPort("x", 0));

                // Trigger the viewPortChanged event to update the display if the axis scale has 
                // changed or if new data are added to the existing axis scale.
                if (axisScaleHasChanged || (duration < initialFullRange))
                    viewer.updateViewPort(true, false);
            }
        }

        //
        // The ViewPortChanged event handler. This event occurs if the user scrolls or zooms in
        // or out the chart by dragging or clicking on the chart. It can also be triggered by
        // calling WinChartViewer.updateViewPort.
        //
        private void WPFChartViewer1_ViewPortChanged(object sender, WPFViewPortEventArgs e)
        {
            var viewer = sender as WPFChartViewer;

            // In addition to updating the chart, we may also need to update other controls that
            // changes based on the view port.
            updateControls(viewer);

            // Update the chart if necessary
            if (e.NeedUpdateChart)
                drawChart(viewer);
        }

        //
        // Update other controls when the view port changed
        //
        private void updateControls(WPFChartViewer viewer)
        {
            // Update the scroll bar to reflect the view port position and width.
            hScrollBar1.IsEnabled = viewer.ViewPortWidth < 1;
            hScrollBar1.LargeChange = viewer.ViewPortWidth * (hScrollBar1.Maximum - hScrollBar1.Minimum);
            hScrollBar1.SmallChange = hScrollBar1.LargeChange * 0.1;
            hScrollBar1.ViewportSize = viewer.ViewPortWidth / Math.Max(1E-10, 1 - viewer.ViewPortWidth)
                * (hScrollBar1.Maximum - hScrollBar1.Minimum);
            hScrollBar1.Value = viewer.ViewPortLeft / Math.Max(1E-10, 1 - viewer.ViewPortWidth)
                * (hScrollBar1.Maximum - hScrollBar1.Minimum) + hScrollBar1.Minimum;
        }

        //
        // Draw the chart.
        //
        private void drawChart(WPFChartViewer viewer)
        {
            // Get the start date and end date that are visible on the chart.
            DateTime viewPortStartDate = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft));
            DateTime viewPortEndDate = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft +
                viewer.ViewPortWidth));

            // Extract the part of the data arrays that are visible.
            DateTime[] viewPortTimeStamps = null;
            double[] viewPortDataSeriesA = null;
            double[] viewPortDataSeriesB = null;
            double[] viewPortDataSeriesC = null;

            if (currentIndex > 0)
            {
                // Get the array indexes that corresponds to the visible start and end dates
                int startIndex = (int)Math.Floor(Chart.bSearch2(timeStamps, 0, currentIndex, viewPortStartDate));
                int endIndex = (int)Math.Ceiling(Chart.bSearch2(timeStamps, 0, currentIndex, viewPortEndDate));
                int noOfPoints = endIndex - startIndex + 1;

                // Extract the visible data
                viewPortTimeStamps = (DateTime[])Chart.arraySlice(timeStamps, startIndex, noOfPoints);
                viewPortDataSeriesA = (double[])Chart.arraySlice(dataSeriesA, startIndex, noOfPoints);
                viewPortDataSeriesB = (double[])Chart.arraySlice(dataSeriesB, startIndex, noOfPoints);
                viewPortDataSeriesC = (double[])Chart.arraySlice(dataSeriesC, startIndex, noOfPoints);
            }

            //
            // At this stage, we have extracted the visible data. We can use those data to plot the chart.
            //

            //================================================================================
            // Configure overall chart appearance.
            //================================================================================

            // Create an XYChart object of size 640 x 350 pixels
            XYChart c = new XYChart(640, 350);

            // Set the plotarea at (55, 50) with width 80 pixels less than chart width, and height 85 pixels
            // less than chart height. Use a vertical gradient from light blue (f0f6ff) to sky blue (a0c0ff)
            // as background. Set border to transparent and grid lines to white (ffffff).
            c.setPlotArea(55, 50, c.getWidth() - 85, c.getHeight() - 80, c.linearGradientColor(0, 50, 0,
                c.getHeight() - 35, 0xf0f6ff, 0xa0c0ff), -1, Chart.Transparent, 0xffffff, 0xffffff);

            // As the data can lie outside the plotarea in a zoomed chart, we need enable clipping.
            c.setClipping();

            // Add a title to the chart using 18 pts Times New Roman Bold Italic font
            c.addTitle("  Realtime Chart with Zoom/Scroll and Track Line", "Arial", 18);

            // Add a legend box at (55, 25) using horizontal layout. Use 8pts Arial Bold as font. Set the
            // background and border color to Transparent and use line style legend key.
            LegendBox b = c.addLegend(55, 25, false, "Arial Bold", 10);
            b.setBackground(Chart.Transparent);
            b.setLineStyleKey();

            // Set the x and y axis stems to transparent and the label font to 10pt Arial
            c.xAxis().setColors(Chart.Transparent);
            c.yAxis().setColors(Chart.Transparent);
            c.xAxis().setLabelStyle("Arial", 10);
            c.yAxis().setLabelStyle("Arial", 10);

            // Add axis title using 10pts Arial Bold Italic font
            c.yAxis().setTitle("Ionic Temperature (C)", "Arial Bold", 10);

            //================================================================================
            // Add data to chart
            //================================================================================

            //
            // In this example, we represent the data by lines. You may modify the code below to use other
            // representations (areas, scatter plot, etc).
            //

            // Add a line layer for the lines, using a line width of 2 pixels
            LineLayer layer = c.addLineLayer2();
            layer.setLineWidth(2);
            layer.setFastLineMode();

            // Now we add the 3 data series to a line layer, using the color red (ff0000), green (00cc00)
            // and blue (0000ff)
            layer.setXData(viewPortTimeStamps);
            layer.addDataSet(viewPortDataSeriesA, 0xff0000, "Alpha");
            layer.addDataSet(viewPortDataSeriesB, 0x00cc00, "Beta");
            layer.addDataSet(viewPortDataSeriesC, 0x0000ff, "Gamma");

            //================================================================================
            // Configure axis scale and labelling
            //================================================================================

            if (currentIndex > 0)
                c.xAxis().setDateScale(viewPortStartDate, viewPortEndDate);

            // For the automatic axis labels, set the minimum spacing to 75/30 pixels for the x/y axis.
            c.xAxis().setTickDensity(75);
            c.yAxis().setTickDensity(30);

            //
            // In a zoomable chart, the time range can be from a few years to a few seconds. We can need
            // to define the date/time format the various cases. 
            //

            // If all ticks are year aligned, we use "yyyy" as the label format.
            c.xAxis().setFormatCondition("align", 360 * 86400);
            c.xAxis().setLabelFormat("{value|yyyy}");

            // If all ticks are month aligned, we use "mmm yyyy" in bold font as the first label of a year,
            // and "mmm" for other labels.
            c.xAxis().setFormatCondition("align", 30 * 86400);
            c.xAxis().setMultiFormat(Chart.StartOfYearFilter(), "<*font=bold*>{value|mmm yyyy}",
                Chart.AllPassFilter(), "{value|mmm}");

            // If all ticks are day algined, we use "mmm dd<*br*>yyyy" in bold font as the first label of a
            // year, and "mmm dd" in bold font as the first label of a month, and "dd" for other labels.
            c.xAxis().setFormatCondition("align", 86400);
            c.xAxis().setMultiFormat(Chart.StartOfYearFilter(),
                "<*block,halign=left*><*font=bold*>{value|mmm dd<*br*>yyyy}", Chart.StartOfMonthFilter(),
                "<*font=bold*>{value|mmm dd}");
            c.xAxis().setMultiFormat2(Chart.AllPassFilter(), "{value|dd}");

            // If all ticks are hour algined, we use "hh:nn<*br*>mmm dd" in bold font as the first label of 
            // the Day, and "hh:nn" for other labels.
            c.xAxis().setFormatCondition("align", 3600);
            c.xAxis().setMultiFormat(Chart.StartOfDayFilter(), "<*font=bold*>{value|hh:nn<*br*>mmm dd}",
                Chart.AllPassFilter(), "{value|hh:nn}");

            // If all ticks are minute algined, then we use "hh:nn" as the label format.
            c.xAxis().setFormatCondition("align", 60);
            c.xAxis().setLabelFormat("{value|hh:nn}");

            // If all other cases, we use "hh:nn:ss" as the label format.
            c.xAxis().setFormatCondition("else");
            c.xAxis().setLabelFormat("{value|hh:nn:ss}");

            // We make sure the tick increment must be at least 1 second.
            c.xAxis().setMinTickInc(1);

            //================================================================================
            // Output the chart
            //================================================================================

            // We need to update the track line too. If the mouse is moving on the chart (eg. if 
            // the user drags the mouse on the chart to scroll it), the track line will be updated
            // in the MouseMovePlotArea event. Otherwise, we need to update the track line here.
            if (!viewer.IsInMouseMoveEvent)
            {
                trackLineLabel(c, (null == viewer.Chart) ? c.getPlotArea().getRightX() :
                    viewer.PlotAreaMouseX);
            }

            viewer.Chart = c;
        }

        //
        // Draw track line with data labels
        //
        private void trackLineLabel(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);
            if (xCoor < plotArea.getLeftX())
                return;

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

            // Draw a label on the x-axis to show the track line position.
            string xlabel = "<*font,bgColor=000000*> " + c.xAxis().getFormattedLabel(xValue, "hh:nn:ss.ff") +
                " <*/font*>";
            TTFText t = d.text(xlabel, "Arial Bold", 10);

            // Restrict the x-pixel position of the label to make sure it stays inside the chart image.
            int xLabelPos = Math.Max(0, Math.Min(xCoor - t.getWidth() / 2, c.getWidth() - t.getWidth()));
            t.draw(xLabelPos, plotArea.getBottomY() + 6, 0xffffff);

            // Iterate through all layers to draw the data labels
            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);

                    // Get the color and position of the data label
                    int color = dataSet.getDataColor();
                    int yCoor = c.getYCoor(dataSet.getPosition(xIndex), dataSet.getUseYAxis());

                    // Draw a track dot with a label next to it for visible data points in the plot area
                    if ((yCoor >= plotArea.getTopY()) && (yCoor <= plotArea.getBottomY()) && (color !=
                        Chart.Transparent) && (!string.IsNullOrEmpty(dataSet.getDataName())))
                    {
                        d.circle(xCoor, yCoor, 4, 4, color, color);

                        string label = "<*font,bgColor=" + color.ToString("x") + "*> " + c.formatValue(
                            dataSet.getValue(xIndex), "{value|P4}") + " <*/font*>";
                        t = d.text(label, "Arial Bold", 10);

                        // Draw the label on the right side of the dot if the mouse is on the left side the
                        // chart, and vice versa. This ensures the label will not go outside the chart image.
                        if (xCoor <= (plotArea.getLeftX() + plotArea.getRightX()) / 2)
                            t.draw(xCoor + 5, yCoor, 0xffffff, Chart.Left);
                        else
                            t.draw(xCoor - 5, yCoor, 0xffffff, Chart.Right);
                    }
                }
            }
        }

        //
        // 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 scroll bar event handler
        //
        private void hScrollBar1_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            var viewer = WPFChartViewer1;

            // When the view port is changed (user drags on the chart to scroll), the scroll bar will get
            // updated. When the scroll bar changes (eg. user drags on the scroll bar), the view port will
            // get updated. This creates an infinite loop. To avoid this, the scroll bar can update the 
            // view port only if the view port is not updating the scroll bar.
            if (!viewer.IsInViewPortChangedEvent)
            {
                // Set the view port based on the scroll bar
                viewer.ViewPortLeft = (hScrollBar1.Value - hScrollBar1.Minimum)
                    / (hScrollBar1.Maximum - hScrollBar1.Minimum) * (1 - viewer.ViewPortWidth);

                // Trigger a view port changed event to update the chart
                viewer.updateViewPort(true, false);
            }
        }

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

        //
        // Pointer (Drag to Scroll) button event handler
        //
        private void pointerPB_Checked(object sender, RoutedEventArgs e)
        {
            WPFChartViewer1.MouseUsage = WinChartMouseUsage.ScrollOnDrag;
        }

        //
        // Zoom In button event handler
        //
        private void zoomInPB_Checked(object sender, RoutedEventArgs e)
        {
            WPFChartViewer1.MouseUsage = WinChartMouseUsage.ZoomIn;
        }

        //
        // Zoom Out button event handler
        //
        private void zoomOutPB_Checked(object sender, RoutedEventArgs e)
        {
            WPFChartViewer1.MouseUsage = WinChartMouseUsage.ZoomOut;
        }

        //
        // Save button event handler
        //
        private void savePB_Click(object sender, RoutedEventArgs e)
        {
            // The standard Save File dialog
            SaveFileDialog fileDlg = new SaveFileDialog();
            fileDlg.Filter = "PNG (*.png)|*.png|JPG (*.jpg)|*.jpg|GIF (*.gif)|*.gif|BMP (*.bmp)|*.bmp|" +
                "SVG (*.svg)|*.svg|PDF (*.pdf)|*.pdf";
            fileDlg.FileName = "chartdirector_demo";
            var ret = fileDlg.ShowDialog(this);
            if (!(ret.HasValue && ret.Value))
                return;

            // Save the chart
            if (null != WPFChartViewer1.Chart)
                WPFChartViewer1.Chart.makeChart(fileDlg.FileName);
        }
    }
}