ChartDirector 7.1 (.NET Edition)

Real-Time MultiChart (Windows)




NOTE: This section describes Real-Time MultiChart for Windows applications only. For web applications, please refer to Real-Time MultiChart (Web).

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 real-time multichart with track cursor.

The example is modified from Realtime Chart with Zooming and Scrolling (Windows) with the following changes:

Source Code Listing

[Windows Forms - C# version] NetWinCharts\CSharpWinCharts\frmrealtimemultichart.cs
using System; using System.Collections.Generic; using System.Windows.Forms; using ChartDirector; namespace CSharpChartExplorer { public partial class FrmRealTimeMultiChart : Form { // // The height of each XYChart. The bottom chart has an extra height for the x-axis labels. // private const int chartHeight = 120; private const int xAxisHeight = 25; // 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 FrmRealTimeMultiChart() { InitializeComponent(); } private void FrmRealTimeMultiChart_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.ViewPortRight < 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) drawMultiChart(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 = viewer.ViewPortWidth < 1; hScrollBar1.LargeChange = (int)Math.Ceiling(viewer.ViewPortWidth * (hScrollBar1.Maximum - hScrollBar1.Minimum)); hScrollBar1.SmallChange = (int)Math.Ceiling(hScrollBar1.LargeChange * 0.1); hScrollBar1.Value = (int)Math.Round(viewer.ViewPortLeft * (hScrollBar1.Maximum - hScrollBar1.Minimum)) + hScrollBar1.Minimum; } // // Draw a single chart // XYChart drawXYChart(WinChartViewer viewer, double[] dataSeries, string name, int color, Axis xAxisScale, bool xAxisVisible) { // 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.ViewPortRight)); // Extract the part of the data arrays that are visible. DateTime[] viewPortTimeStamps = null ; double[] viewPortDataSeries = 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 = Chart.arraySlice(timeStamps, startIndex, noOfPoints); viewPortDataSeries = Chart.arraySlice(dataSeries, startIndex, noOfPoints); } // // At this stage, we have extracted the visible data. We can use those data to plot the chart. // //================================================================================ // Configure overall chart appearance. //================================================================================ // Only the last chart has an x-axis int extraHeght = xAxisVisible ? xAxisHeight : 0; // Create an XYChart object of size 640 x 150 pixels (or 180 pixels for the last chart) XYChart c = new XYChart(640, chartHeight + extraHeght); // Set the plotarea at (55, 10) with width 80 pixels less than chart width, and height 20 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, 10, c.getWidth() - 85, c.getHeight() - 20 - extraHeght, c.linearGradientColor(0, 10, 0, c.getHeight() - 20 - extraHeght, 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 legend box at (55, 5) 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, 5, 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(name, "Arial Bold", 10); //================================================================================ // Add data to chart //================================================================================ // Add a line layer with the given data, with a line width of 2 pixels. LineLayer layer = c.addLineLayer(); layer.setLineWidth(2); layer.setXData(viewPortTimeStamps); layer.addDataSet(viewPortDataSeries, color, name); //================================================================================ // Configure axis scale and labelling //================================================================================ // For the automatic axis labels, set the minimum spacing to 30 pixels for the y axis. c.yAxis().setTickDensity(30); if (null != xAxisScale) { // If xAxisScale is given, then use it to synchronize with other charts. c.xAxis().copyAxis(xAxisScale); } else if (currentIndex > 0) { // If xAxisScale is null, this is the first chart, and it needs to set up the axis scale. c.xAxis().setDateScale(viewPortStartDate, viewPortEndDate); // For the automatic axis labels, set the minimum spacing to 75 pixels for the x axis. c.xAxis().setTickDensity(75); // // In this example, the axis range can change from a few seconds to thousands of seconds. // We can need to define the axis label format for the various cases. // // 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); } // Hide the x-axis if it is not visible. if (!xAxisVisible) c.xAxis().setColors(Chart.Transparent, Chart.Transparent); //================================================================================ // Output the chart //================================================================================ return c; } // // Draw the MultiChart // void drawMultiChart(WinChartViewer viewer) { // The MultiChart contains 3 charts. The x-axis is only visible on the last chart, so we only // need to reserve space for 1 x-axis. MultiChart m = new MultiChart(640, 30 + 3 * chartHeight + xAxisHeight); m.addTitle("Real-Time MultiChart with Zoom/Scroll and Track Line", "Arial", 16); // This first chart is responsible for setting up the x-axis scale. m.addChart(0, 30, drawXYChart(viewer, dataSeriesA, "Alpha", 0xff0000, null, false)); Axis xAxisScale = ((XYChart)m.getChart(0)).xAxis(); // All other charts synchronize their x-axes with that of the first chart. m.addChart(0, 30 + chartHeight, drawXYChart(viewer, dataSeriesB, "Beta", 0x008800, xAxisScale, false)); // The last chart displays the x-axis. m.addChart(0, 30 + chartHeight * 2, drawXYChart(viewer, dataSeriesC, "Gamma", 0x0000ff, xAxisScale, true)); // We need to update the track line too. If the mouse is moving on the chart, the track line // will be updated in MouseMovePlotArea. Otherwise, we need to update the track line here. if (!viewer.IsInMouseMoveEvent) drawMultiTrackLine(m, (null == viewer.Chart) ? m.getWidth() : viewer.PlotAreaMouseX); // Set the combined plot area to be the bounding box of the plot areas of the 3 charts m.setMainChart(m); // Display the chart viewer.Chart = m; } // // Draw track cursor for MultiChart // void drawMultiTrackLine(MultiChart m, int mouseX) { // Obtain the dynamic layer of the MultiChart DrawArea d = m.initDynamicLayer(); // Ask each XYChart to draw the track cursor on the dynamic layer for (int i = 0; i < m.getChartCount(); ++i) drawXYTrackLine(d, (XYChart)m.getChart(i), mouseX, i == m.getChartCount() - 1); } // // Draw track line with data labels // void drawXYTrackLine(DrawArea d, XYChart c, int mouseX, bool hasXAxis) { // In a MultiChart, the XYChart is offsetted from the dynamic layer of the MultiChart int offsetY = c.getAbsOffsetY(); // 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() + offsetY, plotArea.getBottomY() + offsetY, xCoor, 0x888888); // Draw a label on the x-axis to show the track line position. if (hasXAxis) { 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 + offsetY, 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) { DataSet dataSet = layer.getDataSetByZ(j); string dataSetName = dataSet.getDataName(); // 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(dataSetName)) { d.circle(xCoor, yCoor + offsetY, 4, 4, color, color); string label = "<*font,bgColor=" + color.ToString("x") + "*> " + c.formatValue(dataSet.getValue(xIndex), "{value|P4}") + " <*font*>"; TTFText 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 + 6, yCoor + offsetY, 0xffffff, Chart.Left); else t.draw(xCoor - 6, yCoor + offsetY, 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; drawMultiTrackLine((MultiChart)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\frmrealtimemultichart.vb
Imports ChartDirector Public Class FrmRealTimeMultiChart ' ' The height of each XYChart. The bottom chart has an extra height for the x-axis labels. ' Private Const chartHeight As Integer = 120 Private Const xAxisHeight As Integer = 25 ' 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.ViewPortRight < 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 drawMultiChart(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 = viewer.ViewPortWidth < 1 hScrollBar1.LargeChange = Math.Ceiling(viewer.ViewPortWidth * (hScrollBar1.Maximum - hScrollBar1.Minimum)) hScrollBar1.SmallChange = Math.Ceiling(hScrollBar1.LargeChange * 0.1) hScrollBar1.Value = Math.Round(viewer.ViewPortLeft * (hScrollBar1.Maximum - hScrollBar1.Minimum)) + hScrollBar1.Minimum End Sub ' ' Draw a single chart ' Private Function drawXYChart(viewer As WinChartViewer, dataSeries() As Double, name As String, color As Integer, xAxisScale As Axis, xAxisVisible As Boolean) As XYChart ' Get the start date And end date that are visible on the chart. Dim viewPortStartDate As Date = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft)) Dim viewPortEndDate As Date = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortRight)) ' Extract the part of the data arrays that are visible. Dim viewPortTimeStamps() As Date = Nothing Dim viewPortDataSeries() 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) viewPortDataSeries = Chart.arraySlice(dataSeries, 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. '================================================================================ ' Only the last chart has an x-axis Dim extraHeght As Integer = IIf(xAxisVisible, xAxisHeight, 0) ' Create an XYChart object of size 640 x 150 pixels (Or 180 pixels for the last chart) Dim c As XYChart = New XYChart(640, chartHeight + extraHeght) ' Set the plotarea at (55, 10) with width 80 pixels less than chart width, And height 20 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, 10, c.getWidth() - 85, c.getHeight() - 20 - extraHeght, c.linearGradientColor(0, 10, 0, c.getHeight() - 20 - extraHeght, &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 legend box at (55, 5) 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, 5, 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(name, "Arial Bold", 10) '================================================================================ ' Add data to chart '================================================================================ ' Add a line layer with the given data, with a line width of 2 pixels. Dim layer As LineLayer = c.addLineLayer() layer.setLineWidth(2) layer.setXData(viewPortTimeStamps) layer.addDataSet(viewPortDataSeries, color, name) '================================================================================ ' Configure axis scale And labelling '================================================================================ ' For the automatic axis labels, set the minimum spacing to 30 pixels for the y axis. c.yAxis().setTickDensity(30) If Not IsNothing(xAxisScale) Then ' If xAxisScale Is given, then use it to synchronize with other charts. c.xAxis().copyAxis(xAxisScale) ElseIf currentIndex > 0 Then ' If xAxisScale Is null, this Is the first chart, And it needs to set up the axis scale. c.xAxis().setDateScale(viewPortStartDate, viewPortEndDate) ' For the automatic axis labels, set the minimum spacing to 75 pixels for the x axis. c.xAxis().setTickDensity(75) ' ' In this example, the axis range can change from a few seconds to thousands of seconds. ' We can need to define the axis label format for the various cases. ' ' 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) End If ' Hide the x-axis if it Is Not visible. If Not xAxisVisible Then c.xAxis().setColors(Chart.Transparent, Chart.Transparent) End If '================================================================================ ' Output the chart '================================================================================ Return c End Function ' ' Draw the MultiChart ' Private Sub drawMultiChart(viewer As WinChartViewer) ' The MultiChart contains 3 charts. The x-axis Is only visible on the last chart, so we only ' need to reserve space for 1 x-axis. Dim m As MultiChart = New MultiChart(640, 30 + 3 * chartHeight + xAxisHeight) m.addTitle("Real-Time MultiChart with Zoom/Scroll and Track Line", "Arial", 16) ' This first chart Is responsible for setting up the x-axis scale. m.addChart(0, 30, drawXYChart(viewer, dataSeriesA, "Alpha", &HFF0000, Nothing, False)) Dim xAxisScale As Axis = DirectCast(m.getChart(0), XYChart).xAxis() ' All other charts synchronize their x-axes with that of the first chart. m.addChart(0, 30 + chartHeight, drawXYChart(viewer, dataSeriesB, "Beta", &H8800, xAxisScale, False)) ' The last chart displays the x-axis. m.addChart(0, 30 + chartHeight * 2, drawXYChart(viewer, dataSeriesC, "Gamma", &HFF, xAxisScale, True)) ' We need to update the track line too. If the mouse Is moving on the chart, the track line ' will be updated in MouseMovePlotArea. Otherwise, we need to update the track line here. If Not viewer.IsInMouseMoveEvent Then drawMultiTrackLine(m, IIf(IsNothing(viewer.Chart), m.getWidth(), viewer.PlotAreaMouseX)) End If ' Set the combined plot area to be the bounding box of the plot areas of the 3 charts m.setMainChart(m) ' Display the chart viewer.Chart = m End Sub ' ' Draw track cursor for MultiChart ' Private Sub drawMultiTrackLine(m As MultiChart, mouseX As Integer) ' Obtain the dynamic layer of the MultiChart Dim d As DrawArea = m.initDynamicLayer() ' Ask each XYChart to draw the track cursor on the dynamic layer For i As Integer = 0 To m.getChartCount() - 1 drawXYTrackLine(d, m.getChart(i), mouseX, i = m.getChartCount() - 1) Next End Sub ' ' Draw track line with data labels ' Private Sub drawXYTrackLine(d As DrawArea, c As XYChart, mouseX As Integer, hasXAxis As Boolean) ' In a MultiChart, the XYChart Is offsetted from the dynamic layer of the MultiChart Dim offsetY As Integer = c.getAbsOffsetY() ' 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 Exit Sub End If ' Draw a vertical track line at the x-position d.vline(plotArea.getTopY() + offsetY, plotArea.getBottomY() + offsetY, xCoor, &H888888) ' Draw a label on the x-axis to show the track line position. If hasXAxis Then 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 + offsetY, &HFFFFFF) End If ' 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 DataSet = Layer.getDataSetByZ(j) Dim dataSetName As String = dataSet.getDataName() ' 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()) AndAlso (yCoor <= plotarea.getBottomY()) AndAlso (color <> Chart.Transparent) AndAlso Not String.IsNullOrEmpty(dataSetName)) Then d.circle(xCoor, yCoor + offsetY, 4, 4, color, color) Dim label As String = "<*font,bgColor=" & color.ToString("x") & "*> " & c.formatValue(dataSet.getValue(xIndex), "{value|P4}") & " <*font*>" Dim t As TTFText = 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 + 6, yCoor + offsetY, &HFFFFFF, Chart.Left) Else t.draw(xCoor - 6, yCoor + offsetY, &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 drawMultiTrackLine(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\CSharpWPFCharts\RealTimeMultiChartWindow.xaml
<Window x:Class="CSharpWPFCharts.RealTimeMultiChartWindow" 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:CSharpWPFCharts" mc:Ignorable="d" xmlns:ChartDirector="clr-namespace:ChartDirector;assembly=netchartdir" UseLayoutRounding="True" Title="Real Time MultiChart 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="/icons/scroll_icon.png" 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="/icons/zoomin_icon.png" 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="/icons/zoomout_icon.png" 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="/icons/save_icon.png" Height="16" /> <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 IsSelected="True">250</ComboBoxItem> <ComboBoxItem>500</ComboBoxItem> <ComboBoxItem>750</ComboBoxItem> <ComboBoxItem>1000</ComboBoxItem> <ComboBoxItem>1250</ComboBoxItem> <ComboBoxItem>1500</ComboBoxItem> <ComboBoxItem>1750</ComboBoxItem> <ComboBoxItem>2000</ComboBoxItem> </ComboBox> <TextBlock Text="Simulated Machine" Margin="3,90,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="415" 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\CSharpWPFCharts\RealTimeMultiChartWindow.xaml.cs
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Threading; using Microsoft.Win32; using ChartDirector; namespace CSharpWPFCharts { /// <summary> /// Interaction logic for RealTimeMultiChartWindow.xaml /// </summary> public partial class RealTimeMultiChartWindow : Window { // // The height of each XYChart. The bottom chart has an extra height for the x-axis labels. // private const int chartHeight = 120; private const int xAxisHeight = 25; // 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 RealTimeMultiChartWindow() { 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; // 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[Math.Max(0, 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.ViewPortRight < 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) drawMultiChart(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 a single chart // XYChart drawXYChart(WPFChartViewer viewer, double[] dataSeries, string name, int color, Axis xAxisScale, bool xAxisVisible) { // 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.ViewPortRight)); // Extract the part of the data arrays that are visible. DateTime[] viewPortTimeStamps = null; double[] viewPortDataSeries = 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 = Chart.arraySlice(timeStamps, startIndex, noOfPoints); viewPortDataSeries = Chart.arraySlice(dataSeries, startIndex, noOfPoints); } // // At this stage, we have extracted the visible data. We can use those data to plot the chart. // //================================================================================ // Configure overall chart appearance. //================================================================================ // Only the last chart has an x-axis int extraHeght = xAxisVisible ? xAxisHeight : 0; // Create an XYChart object of size 640 x 150 pixels (or 180 pixels for the last chart) XYChart c = new XYChart(640, chartHeight + extraHeght); // Set the plotarea at (55, 10) with width 80 pixels less than chart width, and height 20 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, 10, c.getWidth() - 85, c.getHeight() - 20 - extraHeght, c.linearGradientColor(0, 10, 0, c.getHeight() - 20 - extraHeght, 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 legend box at (55, 5) 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, 5, 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(name, "Arial Bold", 10); //================================================================================ // Add data to chart //================================================================================ // Add a line layer with the given data, with a line width of 2 pixels. LineLayer layer = c.addLineLayer(); layer.setLineWidth(2); layer.setXData(viewPortTimeStamps); layer.addDataSet(viewPortDataSeries, color, name); //================================================================================ // Configure axis scale and labelling //================================================================================ // For the automatic axis labels, set the minimum spacing to 30 pixels for the y axis. c.yAxis().setTickDensity(30); if (null != xAxisScale) { // If xAxisScale is given, then use it to synchronize with other charts. c.xAxis().copyAxis(xAxisScale); } else if (currentIndex > 0) { // If xAxisScale is null, this is the first chart, and it needs to set up the axis scale. c.xAxis().setDateScale(viewPortStartDate, viewPortEndDate); // For the automatic axis labels, set the minimum spacing to 75 pixels for the x axis. c.xAxis().setTickDensity(75); // // In this example, the axis range can change from a few seconds to thousands of seconds. // We can need to define the axis label format for the various cases. // // 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); } // Hide the x-axis if it is not visible. if (!xAxisVisible) c.xAxis().setColors(Chart.Transparent, Chart.Transparent); //================================================================================ // Output the chart //================================================================================ return c; } // // Draw the MultiChart // void drawMultiChart(WPFChartViewer viewer) { // The MultiChart contains 3 charts. The x-axis is only visible on the last chart, so we only // need to reserve space for 1 x-axis. MultiChart m = new MultiChart(640, 30 + 3 * chartHeight + xAxisHeight); m.addTitle("Real-Time MultiChart with Zoom/Scroll and Track Line", "Arial", 16); // This first chart is responsible for setting up the x-axis scale. m.addChart(0, 30, drawXYChart(viewer, dataSeriesA, "Alpha", 0xff0000, null, false)); Axis xAxisScale = ((XYChart)m.getChart(0)).xAxis(); // All other charts synchronize their x-axes with that of the first chart. m.addChart(0, 30 + chartHeight, drawXYChart(viewer, dataSeriesB, "Beta", 0x008800, xAxisScale, false)); // The last chart displays the x-axis. m.addChart(0, 30 + chartHeight * 2, drawXYChart(viewer, dataSeriesC, "Gamma", 0x0000ff, xAxisScale, true)); // We need to update the track line too. If the mouse is moving on the chart, the track line // will be updated in MouseMovePlotArea. Otherwise, we need to update the track line here. if (!viewer.IsInMouseMoveEvent) drawMultiTrackLine(m, (null == viewer.Chart) ? m.getWidth() : viewer.PlotAreaMouseX); // Set the combined plot area to be the bounding box of the plot areas of the 3 charts m.setMainChart(m); // Display the chart viewer.Chart = m; } // // Draw track cursor for MultiChart // void drawMultiTrackLine(MultiChart m, int mouseX) { // Obtain the dynamic layer of the MultiChart DrawArea d = m.initDynamicLayer(); // Ask each XYChart to draw the track cursor on the dynamic layer for (int i = 0; i < m.getChartCount(); ++i) drawXYTrackLine(d, (XYChart)m.getChart(i), mouseX, i == m.getChartCount() - 1); } // // Draw track line with data labels // void drawXYTrackLine(DrawArea d, XYChart c, int mouseX, bool hasXAxis) { // In a MultiChart, the XYChart is offsetted from the dynamic layer of the MultiChart int offsetY = c.getAbsOffsetY(); // 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() + offsetY, plotArea.getBottomY() + offsetY, xCoor, 0x888888); // Draw a label on the x-axis to show the track line position. if (hasXAxis) { 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 + offsetY, 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) { DataSet dataSet = layer.getDataSetByZ(j); string dataSetName = dataSet.getDataName(); // 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(dataSetName)) { d.circle(xCoor, yCoor + offsetY, 4, 4, color, color); string label = "<*font,bgColor=" + color.ToString("x") + "*> " + c.formatValue(dataSet.getValue(xIndex), "{value|P4}") + " <*font*>"; TTFText 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 + 6, yCoor + offsetY, 0xffffff, Chart.Left); else t.draw(xCoor - 6, yCoor + offsetY, 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; drawMultiTrackLine((MultiChart)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); } } }