ChartDirector 7.1 (.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.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) 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 = 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 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.ViewPortRight)); // 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 = Chart.arraySlice(timeStamps, startIndex, noOfPoints); viewPortDataSeriesA = Chart.arraySlice(dataSeriesA, startIndex, noOfPoints); viewPortDataSeriesB = Chart.arraySlice(dataSeriesB, startIndex, noOfPoints); viewPortDataSeriesC = 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.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 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 = 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 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.ViewPortRight)) ' 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\CSharpWPFCharts\RealTimeZoomScrollWindow.xaml
<Window x:Class="CSharpWPFCharts.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:CSharpWPFCharts" 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="/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,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\CSharpWPFCharts\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 CSharpWPFCharts { /// <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.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) 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.ViewPortRight)); // 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 = Chart.arraySlice(timeStamps, startIndex, noOfPoints); viewPortDataSeriesA = Chart.arraySlice(dataSeriesA, startIndex, noOfPoints); viewPortDataSeriesB = Chart.arraySlice(dataSeriesB, startIndex, noOfPoints); viewPortDataSeriesC = 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); } } }