using System; using System.Windows; using System.Windows.Input; using System.Windows.Threading; using System.Windows.Controls; using Microsoft.Win32; using ChartDirector; namespace CSharpWPFCharts { /// /// Interaction logic for RealTimeZoomScroll.xaml /// 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 drawChart(WPFChartViewer1); dataRateTimer.Start(); chartUpdateTimer.Start(); } // // Initialize the WinChartViewer // private void initChartViewer(WPFChartViewer viewer) { // Enable mouse wheel zooming viewer.MouseWheelZoomRatio = 1.1; // Initially set the mouse usage to "Pointer" mode (Drag to Scroll mode) pointerPB.IsChecked = true; } // // The data update routine. In this demo, it is invoked every 250ms to get new data. // private void dataRateTimer_Tick(object sender, EventArgs e) { do { // // In this demo, we use some formulas to generate new values. In real applications, // it may be replaced by some data acquisition code. // double p = nextDataTime.Ticks / 10000000.0 * 4; double dataA = 20 + Math.Cos(p * 2.2) * 10 + 1 / (Math.Cos(p) * Math.Cos(p) + 0.01); double dataB = 150 + 100 * Math.Sin(p / 27.7) * Math.Sin(p / 10.1); double dataC = 150 + 100 * Math.Cos(p / 6.7) * Math.Cos(p / 11.9); // In this demo, if the data arrays are full, the oldest 5% of data are discarded. if (currentIndex >= timeStamps.Length) { currentIndex = sampleSize * 95 / 100 - 1; for (int i = 0; i < currentIndex; ++i) { int srcIndex = i + sampleSize - currentIndex; timeStamps[i] = timeStamps[srcIndex]; dataSeriesA[i] = dataSeriesA[srcIndex]; dataSeriesB[i] = dataSeriesB[srcIndex]; dataSeriesC[i] = dataSeriesC[srcIndex]; } } // Store the new values in the current index position, and increment the index. timeStamps[currentIndex] = nextDataTime; dataSeriesA[currentIndex] = dataA; dataSeriesB[currentIndex] = dataB; dataSeriesC[currentIndex] = dataC; ++currentIndex; nextDataTime = nextDataTime.AddMilliseconds(dataRateTimer.Interval.TotalMilliseconds); } while (nextDataTime < DateTime.Now); // We provide some visual feedback to the numbers generated, so you can see the // values being generated. valueA.Content = dataSeriesA[currentIndex - 1].ToString(".##"); valueB.Content = dataSeriesB[currentIndex - 1].ToString(".##"); valueC.Content = dataSeriesC[currentIndex - 1].ToString(".##"); } // // Update the chart and the viewport periodically // private void chartUpdateTimer_Tick(object sender, EventArgs e) { var viewer = WPFChartViewer1; if (currentIndex > 0) { // // As we added more data, we may need to update the full range. // DateTime startDate = timeStamps[0]; DateTime endDate = timeStamps[currentIndex - 1]; // Use the initialFullRange if this is sufficient. double duration = endDate.Subtract(startDate).TotalSeconds; if (duration < initialFullRange) endDate = startDate.AddSeconds(initialFullRange); // Update the full range to reflect the actual duration of the data. In this case, // if the view port is viewing the latest data, we will scroll the view port as new // data are added. If the view port is viewing historical data, we would keep the // axis scale unchanged to keep the chart stable. int updateType = Chart.ScrollWithMax; if (viewer.ViewPortLeft + viewer.ViewPortWidth < 0.999) updateType = Chart.KeepVisibleRange; bool axisScaleHasChanged = viewer.updateFullRangeH("x", startDate, endDate, updateType); // Set the zoom in limit as a ratio to the full range viewer.ZoomInWidthLimit = zoomInLimit / (viewer.getValueAtViewPort("x", 1) - viewer.getValueAtViewPort("x", 0)); // Trigger the viewPortChanged event to update the display if the axis scale has // changed or if new data are added to the existing axis scale. if (axisScaleHasChanged || (duration < initialFullRange)) viewer.updateViewPort(true, false); } } // // The ViewPortChanged event handler. This event occurs if the user scrolls or zooms in // or out the chart by dragging or clicking on the chart. It can also be triggered by // calling WinChartViewer.updateViewPort. // private void WPFChartViewer1_ViewPortChanged(object sender, WPFViewPortEventArgs e) { var viewer = sender as WPFChartViewer; // In addition to updating the chart, we may also need to update other controls that // changes based on the view port. updateControls(viewer); // Update the chart if necessary if (e.NeedUpdateChart) drawChart(viewer); } // // Update other controls when the view port changed // private void updateControls(WPFChartViewer viewer) { // Update the scroll bar to reflect the view port position and width. hScrollBar1.IsEnabled = viewer.ViewPortWidth < 1; hScrollBar1.LargeChange = viewer.ViewPortWidth * (hScrollBar1.Maximum - hScrollBar1.Minimum); hScrollBar1.SmallChange = hScrollBar1.LargeChange * 0.1; hScrollBar1.ViewportSize = viewer.ViewPortWidth / Math.Max(1E-10, 1 - viewer.ViewPortWidth) * (hScrollBar1.Maximum - hScrollBar1.Minimum); hScrollBar1.Value = viewer.ViewPortLeft / Math.Max(1E-10, 1 - viewer.ViewPortWidth) * (hScrollBar1.Maximum - hScrollBar1.Minimum) + hScrollBar1.Minimum; } // // Draw the chart. // private XYChart drawChart(WPFChartViewer viewer, double[] dataSeries, string name, int color, bool hasXAxis) { // Get the start date and end date that are visible on the chart. DateTime viewPortStartDate = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft)); DateTime viewPortEndDate = Chart.NTime(viewer.getValueAtViewPort("x", viewer.ViewPortLeft + viewer.ViewPortWidth)); // Extract the part of the data arrays that are visible. DateTime[] viewPortTimeStamps = null; double[] 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 = (DateTime[])Chart.arraySlice(timeStamps, startIndex, noOfPoints); viewPortDataSeries = (double[])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 xAxisHeight = hasXAxis ? 30 : 0; // Create an XYChart object of size 640 x 150 pixels (or 180 pixels for the last chart) XYChart c = new XYChart(640, 150 + xAxisHeight) ; // 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 + xAxisHeight), c.linearGradientColor(0, 10, 0, c.getHeight() - (20 + xAxisHeight), 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 //================================================================================ // // 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(viewPortDataSeries, color, name); //================================================================================ // 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); if (!hasXAxis) c.xAxis().setColors(Chart.Transparent, Chart.Transparent); //================================================================================ // Output the chart //================================================================================ return c; } private void drawChart(WPFChartViewer viewer) { MultiChart m = new MultiChart(640, 3 * 150 + 30); m.addChart(0, 0, drawChart(viewer, dataSeriesA, "Alpha", 0xff0000, false)); m.addChart(0, 150, drawChart(viewer, dataSeriesB, "Beta", 0x00cc00, false)); m.addChart(0, 300, drawChart(viewer, dataSeriesC, "Gamma", 0x0000ff, true)); // 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(m, (null == viewer.Chart) ? m.getWidth() : viewer.PlotAreaMouseX); viewer.Chart = m; } private void trackLineLabel(MultiChart m, int mouseX) { // Clear the current dynamic layer and get the DrawArea object to draw on it. DrawArea d = m.initDynamicLayer(); XYChart c = null; for (int i = 0; (c = (XYChart)m.getChart(i)) != null; ++i) trackLineLabel(c, mouseX, (XYChart)m.getChart(i + 1) == null, d); } // // Draw track line with data labels // private void trackLineLabel(XYChart c, int mouseX, bool hasXAxis, DrawArea d) { // In a MultiChart, the XYChart is offsetet 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) { 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 + 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 + 5, yCoor + offsetY, 0xffffff, Chart.Left); else t.draw(xCoor - 5, 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 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((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; chartUpdateTimer.Stop(); currentIndex = 0; drawChart(WPFChartViewer1); } // // Zoom Out button event handler // private void zoomOutPB_Checked(object sender, RoutedEventArgs e) { WPFChartViewer1.MouseUsage = WinChartMouseUsage.ZoomOut; currentIndex = 0; chartUpdateTimer.Start(); } // // 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); } } }