ChartDirector 7.1 (C++ Edition)

Real-Time MultiChart (MFC)




NOTE: This section describes Real-Time MultiChart for MFC only. For Qt, please refer to Real-Time MultiChart (Qt).

This example demonstrates a zoomable and scrollable real-time multichart with track cursor.

The example is modified from Real-Time Chart with Zooming and Scrolling (MFC) with the following changes:

Source Code Listing

[MFC version] mfcdemo/RealTimeMultiChartDlg.cpp
// // Real Time Multi-Chart sample code // #include "stdafx.h" #include "resource.h" #include "RealTimeMultiChartDlg.h" #include <math.h> #include <vector> #include <string> #include <algorithm> #include <sstream> #ifdef _DEBUG #define new DEBUG_NEW #endif // // The DataRateTimerId is for the timer that gets real-time data. In real applications, // the data can be updated by a timer or other methods. In this example, this timer is // set to 250ms. // // The ChartUpdateTimerId is for the timer that updates the chart. In this example, // the user can choose the chart update rate from the user interface. // static const int DataRateTimerId = 1; static const int ChartUpdateTimerId = 2; static const int DataInterval = 250; // // The height of each XYChart. The bottom chart has an extra height for the x-axis labels. // static const int chartHeight = 120; static const int xAxisHeight = 25; // // Constructor // CRealTimeMultiChartDlg::CRealTimeMultiChartDlg(CWnd* pParent /*=NULL*/) : CDialog(IDD_REALTIMEMULTICHART, pParent) { // Initialize variables for (int i = 0; i < sampleSize; ++i) m_timeStamps[i] = m_dataSeriesA[i] = m_dataSeriesB[i] = m_dataSeriesC[i] = Chart::NoValue; m_nextDataTime = m_currentIndex = 0; } // // Destructor // CRealTimeMultiChartDlg::~CRealTimeMultiChartDlg() { deleteMultiChart((MultiChart*)m_ChartViewer.getChart()); } void CRealTimeMultiChartDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Control(pDX, IDC_GammaValue, m_ValueC); DDX_Control(pDX, IDC_BetaValue, m_ValueB); DDX_Control(pDX, IDC_AlphaValue, m_ValueA); DDX_Control(pDX, IDC_ChartViewer, m_ChartViewer); DDX_Control(pDX, IDC_UpdatePeriod, m_UpdatePeriod); DDX_Control(pDX, IDC_PointerPB, m_PointerPB); DDX_Control(pDX, IDC_HScrollBar, m_HScrollBar); } BEGIN_MESSAGE_MAP(CRealTimeMultiChartDlg, CDialog) ON_WM_TIMER() ON_CBN_SELCHANGE(IDC_UpdatePeriod, OnSelchangeUpdatePeriod) ON_CONTROL(CVN_ViewPortChanged, IDC_ChartViewer, OnViewPortChanged) ON_CONTROL(CVN_MouseMovePlotArea, IDC_ChartViewer, OnMouseMovePlotArea) ON_BN_CLICKED(IDC_PointerPB, OnPointerPB) ON_BN_CLICKED(IDC_ZoomInPB, OnZoomInPB) ON_BN_CLICKED(IDC_ZoomOutPB, OnZoomOutPB) ON_BN_CLICKED(IDC_SavePB, OnSavePB) ON_WM_HSCROLL() END_MESSAGE_MAP() // // Initialization // BOOL CRealTimeMultiChartDlg::OnInitDialog() { CDialog::OnInitDialog(); // Set m_nextDataTime to the current time. It is used by the real time random number // generator so it knows what timestamp should be used for the next data point. SYSTEMTIME st; GetLocalTime(&st); m_nextDataTime = Chart::chartTime(st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); // Load icons for the buttons loadButtonIcon(IDC_PointerPB, IDI_PointerPB, 100, 20); loadButtonIcon(IDC_ZoomInPB, IDI_ZoomInPB, 100, 20); loadButtonIcon(IDC_ZoomOutPB, IDI_ZoomOutPB, 100, 20); loadButtonIcon(IDC_SavePB, IDI_SavePB, 100, 20); // Initially set the mouse to drag to scroll mode. m_PointerPB.SetCheck(1); m_ChartViewer.setMouseUsage(Chart::MouseUsageScroll); // Enable mouse wheel zooming by setting the zoom ratio to 1.1 per wheel event m_ChartViewer.setMouseWheelZoomRatio(1.1); // Set up the data acquisition mechanism. In this demo, we just use a timer to get a // sample every 250ms. SetTimer(DataRateTimerId, DataInterval, 0); // The chart update rate initially set to 250ms m_UpdatePeriod.SelectString(0, _T("250")); OnSelchangeUpdatePeriod(); return TRUE; } // // User clicks on the Pointer pushbutton // void CRealTimeMultiChartDlg::OnPointerPB() { m_ChartViewer.setMouseUsage(Chart::MouseUsageScroll); } // // User clicks on the Zoom In pushbutton // void CRealTimeMultiChartDlg::OnZoomInPB() { m_ChartViewer.setMouseUsage(Chart::MouseUsageZoomIn); } // // User clicks on the Zoom Out pushbutton // void CRealTimeMultiChartDlg::OnZoomOutPB() { m_ChartViewer.setMouseUsage(Chart::MouseUsageZoomOut); } // // User clicks on the Save pushbutton // void CRealTimeMultiChartDlg::OnSavePB() { // Supported formats = PNG, JPG, GIF, BMP, SVG and PDF TCHAR szFilters[]= _T("PNG (*.png)|*.png|JPG (*.jpg)|*.jpg|GIF (*.gif)|*.gif|") _T("BMP (*.bmp)|*.bmp|SVG (*.svg)|*.svg|PDF (*.pdf)|*.pdf||"); // The standard CFileDialog CFileDialog fileDlg(FALSE, _T("png"), _T("chartdirector_demo"), OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, szFilters); if(fileDlg.DoModal() != IDOK) return; // Save the chart CString path = fileDlg.GetPathName(); BaseChart *c = m_ChartViewer.getChart(); if (0 != c) c->makeChart(TCHARtoUTF8(path)); } // // User clicks on the the horizontal scroll bar // void CRealTimeMultiChartDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { // Update the view port if the scroll bar has moved double newViewPortLeft = moveScrollBar(nSBCode, nPos, pScrollBar); if (newViewPortLeft != m_ChartViewer.getViewPortLeft()) { m_ChartViewer.setViewPortLeft(newViewPortLeft); m_ChartViewer.updateViewPort(true, false); } CDialog::OnHScroll(nSBCode, nPos, pScrollBar); } // // User changes the chart update period // void CRealTimeMultiChartDlg::OnSelchangeUpdatePeriod() { CString s; m_UpdatePeriod.GetLBText(m_UpdatePeriod.GetCurSel(), s); SetTimer(ChartUpdateTimerId, _tcstol(s, 0, 0), 0); } // // Handles timer events // void CRealTimeMultiChartDlg::OnTimer(UINT_PTR nIDEvent) { switch (nIDEvent) { case DataRateTimerId: // Is data acquisition timer OnDataRateTimer(); break; case ChartUpdateTimerId: // Is chart update timer OnChartUpdateTimer(); break; } CDialog::OnTimer(nIDEvent); } // // View port changed event // void CRealTimeMultiChartDlg::OnViewPortChanged() { // In addition to updating the chart, we may also need to update other controls that // changes based on the view port. updateControls(&m_ChartViewer); // Update the chart if necessary if (m_ChartViewer.needUpdateChart()) drawMultiChart(&m_ChartViewer); } // // Draw track cursor when mouse is moving over plotarea // void CRealTimeMultiChartDlg::OnMouseMovePlotArea() { drawMultiTrackLine((MultiChart*)m_ChartViewer.getChart(), m_ChartViewer.getPlotAreaMouseX()); m_ChartViewer.updateDisplay(); } // // The data acquisition routine. In this demo, this is invoked every 250ms. // void CRealTimeMultiChartDlg::OnDataRateTimer() { // The current time in millisecond resolution SYSTEMTIME st; GetLocalTime(&st); double now = Chart::chartTime(st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond) + st.wMilliseconds / 1000.0; // // Use a loop to generate random numbers since the last time this method is called. // do { // In this example, we use some formulas to generate new values. double p = m_nextDataTime * 4; double dataA = 20 + cos(p * 2.2) * 10 + 1 / (cos(p) * cos(p) + 0.01); double dataB = 150 + 100 * sin(p / 27.7) * sin(p / 10.1); double dataC = 150 + 100 * cos(p / 6.7) * cos(p / 11.9); // If the data arrays are full, we remove the oldest 5% of data. if (m_currentIndex >= sampleSize) { m_currentIndex = sampleSize * 95 / 100 - 1; for(int i = 0; i < m_currentIndex; ++i) { int srcIndex = i + sampleSize - m_currentIndex; m_timeStamps[i] = m_timeStamps[srcIndex]; m_dataSeriesA[i] = m_dataSeriesA[srcIndex]; m_dataSeriesB[i] = m_dataSeriesB[srcIndex]; m_dataSeriesC[i] = m_dataSeriesC[srcIndex]; } } // Store the new values in the current index position, and increment the index. m_timeStamps[m_currentIndex] = m_nextDataTime; m_dataSeriesA[m_currentIndex] = dataA; m_dataSeriesB[m_currentIndex] = dataB; m_dataSeriesC[m_currentIndex] = dataC; ++m_currentIndex; m_nextDataTime += DataInterval / 1000.0; } while (m_nextDataTime < now); // // We provide some visual feedback to the latest numbers generated, so you can see the data // being generated. // char buffer[1024]; sprintf_s(buffer, sizeof(buffer), " %.2f", m_dataSeriesA[m_currentIndex - 1]); m_ValueA.SetWindowText(CString(buffer)); sprintf_s(buffer, sizeof(buffer), " %.2f", m_dataSeriesB[m_currentIndex - 1]); m_ValueB.SetWindowText(CString(buffer)); sprintf_s(buffer, sizeof(buffer), " %.2f", m_dataSeriesC[m_currentIndex - 1]); m_ValueC.SetWindowText(CString(buffer)); } // // Update the chart and the viewport periodically // void CRealTimeMultiChartDlg::OnChartUpdateTimer() { if (m_currentIndex > 0) { // // As we added more data, we may need to update the full range of the viewport. // double startDate = m_timeStamps[0]; double endDate = m_timeStamps[m_currentIndex - 1]; // Use the initialFullRange (which is 60 seconds in this demo) if this is sufficient. double duration = endDate - startDate; if (duration < initialFullRange) endDate = startDate + 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 (m_ChartViewer.getViewPortLeft() + m_ChartViewer.getViewPortWidth() < 0.999) updateType = Chart::KeepVisibleRange; bool scaleHasChanged = m_ChartViewer.updateFullRangeH("x", startDate, endDate, updateType); // Set the zoom in limit as a ratio to the full range m_ChartViewer.setZoomInWidthLimit(zoomInLimit / (m_ChartViewer.getValueAtViewPort("x", 1) - m_ChartViewer.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 (scaleHasChanged || (duration < initialFullRange)) m_ChartViewer.updateViewPort(true, false); } } // // Handle scroll bar events // double CRealTimeMultiChartDlg::moveScrollBar(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { // // Get current scroll bar position // SCROLLINFO info; info.cbSize = sizeof(SCROLLINFO); info.fMask = SIF_ALL; pScrollBar->GetScrollInfo(&info); // // Compute new position based on the type of scroll bar events // int newPos = info.nPos; switch (nSBCode) { case SB_LEFT: newPos = info.nMin; break; case SB_RIGHT: newPos = info.nMax; break; case SB_LINELEFT: newPos -= (info.nPage > 10) ? info.nPage / 10 : 1; break; case SB_LINERIGHT: newPos += (info.nPage > 10) ? info.nPage / 10 : 1; break; case SB_PAGELEFT: newPos -= info.nPage; break; case SB_PAGERIGHT: newPos += info.nPage; break; case SB_THUMBTRACK: newPos = info.nTrackPos; break; } if (newPos < info.nMin) newPos = info.nMin; if (newPos > info.nMax) newPos = info.nMax; // Update the scroll bar with the new position pScrollBar->SetScrollPos(newPos); // Returns the position of the scroll bar as a ratio of its total length return ((double)(newPos - info.nMin)) / (info.nMax - info.nMin); } // // Update controls when the view port changed // void CRealTimeMultiChartDlg::updateControls(CChartViewer *viewer) { // Update the scroll bar to reflect the view port position and width of the view port. m_HScrollBar.EnableWindow(viewer->getViewPortWidth() < 1); if (viewer->getViewPortWidth() < 1) { SCROLLINFO info; info.cbSize = sizeof(SCROLLINFO); info.fMask = SIF_ALL; info.nMin = 0; info.nMax = 0x1fffffff; info.nPage = (int)ceil(viewer->getViewPortWidth() * (info.nMax - info.nMin)); info.nPos = (int)(0.5 + viewer->getViewPortLeft() * (info.nMax - info.nMin)) + info.nMin; m_HScrollBar.SetScrollInfo(&info); } } // // Draw a single chart // XYChart* CRealTimeMultiChartDlg::drawXYChart(CChartViewer *viewer, const double* dataSeries, const char *name, int color, Axis* xAxisScale, bool xAxisVisible) { // Get the start date and end date that are visible on the chart. double viewPortStartDate = viewer->getValueAtViewPort("x", viewer->getViewPortLeft()); double viewPortEndDate = viewer->getValueAtViewPort("x", viewer->getViewPortRight()); // Extract the part of the data arrays that are visible. DoubleArray viewPortTimeStamps; DoubleArray viewPortDataSeries; if (m_currentIndex > 0) { // Get the array indexes that corresponds to the visible start and end dates int startIndex = (int)floor(Chart::bSearch(DoubleArray(m_timeStamps, m_currentIndex), viewPortStartDate)); int endIndex = (int)ceil(Chart::bSearch(DoubleArray(m_timeStamps, m_currentIndex), viewPortEndDate)); int noOfPoints = endIndex - startIndex + 1; // Extract the visible data viewPortTimeStamps = DoubleArray(m_timeStamps + startIndex, noOfPoints); viewPortDataSeries = DoubleArray(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 (0 != xAxisScale) { // If xAxisScale is given, then use it to synchronize with other charts. c->xAxis()->copyAxis(xAxisScale); } else if (m_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 CRealTimeMultiChartDlg::drawMultiChart(CChartViewer* 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, m_dataSeriesA, "Alpha", 0xff0000, 0, 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, m_dataSeriesB, "Beta", 0x00cc00, xAxisScale, false)); // The last chart displays the x-axis. m->addChart(0, 30 + chartHeight * 2, drawXYChart(viewer, m_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, (0 == viewer->getChart()) ? m->getWidth() : viewer->getPlotAreaMouseX()); // Set the combined plot area to be the bounding box of the plot areas of the 3 charts m->setMainChart(m); deleteMultiChart((MultiChart*)viewer->getChart()); viewer->setChart(m); } // // Delete the MultiChart and all its subcharts // void CRealTimeMultiChartDlg::deleteMultiChart(MultiChart *m) { if (0 != m) { // Delete all the charts inside the MultiChart, then delete the MultiChart itself. for (int i = 0; i < m->getChartCount(); ++i) delete m->getChart(i); delete m; } } // // Draw track cursor for MultiChart // void CRealTimeMultiChartDlg::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 CRealTimeMultiChartDlg::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) { std::ostringstream xlabel; xlabel << "<*font,bgColor=000000*> " << c->xAxis()->getFormattedLabel(xValue, "hh:nn:ss.ff") << " <*/font*>"; TTFText* t = d->text(xlabel.str().c_str(), "Arial Bold", 10); // Restrict the x-pixel position of the label to make sure it stays inside the chart image. int xLabelPos = (std::max)(0, (std::min)(xCoor - t->getWidth() / 2, c->getWidth() - t->getWidth())); t->draw(xLabelPos, plotArea->getBottomY() + 6 + offsetY, 0xffffff); t->destroy(); } // 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); const char* 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) && dataSetName && *dataSetName) { d->circle(xCoor, yCoor + offsetY, 4, 4, color, color); std::ostringstream label; label << "<*font,bgColor=" << std::hex << color << "*> " << c->formatValue(dataSet->getValue(xIndex), "{value|P4}") << " <*font*>"; TTFText*t = d->text(label.str().c_str(), "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); t->destroy(); } } } } ///////////////////////////////////////////////////////////////////////////// // General utilities // // Load an icon resource into a button // void CRealTimeMultiChartDlg::loadButtonIcon(int buttonId, int iconId, int width, int height) { // Resize the icon to match the screen DPI for high DPI support HDC screen = ::GetDC(0); double scaleFactor = GetDeviceCaps(screen, LOGPIXELSX) / 96.0; ::ReleaseDC(0, screen); width = (int)(width * scaleFactor + 0.5); height = (int)(height * scaleFactor + 0.5); GetDlgItem(buttonId)->SendMessage(BM_SETIMAGE, IMAGE_ICON, (LPARAM)::LoadImage( AfxGetResourceHandle(), MAKEINTRESOURCE(iconId), IMAGE_ICON, width, height, LR_DEFAULTCOLOR)); }