ChartDirector 7.1 (C++ Edition)

Mega Chart Zoom/Scroll (MFC)




NOTE: This section describes Mega Chart Zoom/Scroll for MFC only. For Qt, please refer to Mega Chart Zoom/Scroll (Qt).

This example demonstrates a zoomable and scrollable chart with huge datasets containing 3 x 10M data points.

The zooming, scrolling and track cursor part of the code is similar to Zooming and Scrolling with Track Line (1) (MFC), which you may refer to for more details. In the following explanation, we will focus on the code that handles the huge number of data points.

Source Code Listing

[MFC version] mfcdemo/MegaZoomScrollDlg.cpp
// CMegaZoomScrollDlg.cpp : implementation file // #include "stdafx.h" #include "resource.h" #include "MegaZoomScrollDlg.h" #include <vector> #include <string> #include <sstream> #include <algorithm> #ifdef _DEBUG #define new DEBUG_NEW #endif // In this example, we plot 3 data series, each with 10,000,000 data points. // So the total is 30,000,000 data points. static int bufferSize = 10000000; // // Constructor // CMegaZoomScrollDlg::CMegaZoomScrollDlg(CWnd* pParent /*=nullptr*/) : CDialog(IDD_MEGAZOOMSCROLL, pParent) { m_fastData = 0; m_hasFinishedInitialization = false; } // // Destructor // CMegaZoomScrollDlg::~CMegaZoomScrollDlg() { delete m_ChartViewer.getChart(); delete m_fastData; } void CMegaZoomScrollDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Control(pDX, IDC_ChartViewer, m_ChartViewer); DDX_Control(pDX, IDC_HScrollBar, m_HScrollBar); DDX_Control(pDX, IDC_PointerPB, m_PointerPB); } BEGIN_MESSAGE_MAP(CMegaZoomScrollDlg, CDialog) ON_BN_CLICKED(IDC_PointerPB, OnPointerPB) ON_BN_CLICKED(IDC_ZoomInPB, OnZoomInPB) ON_BN_CLICKED(IDC_ZoomOutPB, OnZoomOutPB) ON_WM_HSCROLL() ON_CONTROL(CVN_ViewPortChanged, IDC_ChartViewer, OnViewPortChanged) ON_CONTROL(CVN_MouseMovePlotArea, IDC_ChartViewer, OnMouseMovePlotArea) ON_BN_CLICKED(IDC_PlotChartPB, OnClickPlotChart) END_MESSAGE_MAP() // // Initialization // BOOL CMegaZoomScrollDlg::OnInitDialog() { CDialog::OnInitDialog(); // Load icons to mouse usage buttons loadButtonIcon(IDC_PointerPB, IDI_Scroll, 20, 20); loadButtonIcon(IDC_ZoomInPB, IDI_ZoomIn, 20, 20); loadButtonIcon(IDC_ZoomOutPB, IDI_ZoomOut, 20, 20); // Generate random data loadData(); // Display initial Message PieChart* c = new PieChart(800, 400, 0xd0e0ff); c->addTitle(Chart::Center, "<*block,halign=left,maxwidth=500*>" "<*font=Arial Bold,size=18,underline=2*>Mega Chart Zoom/Scroll with Track Line<*/font*>" "<*br*><*br*>This example demonstrates a chart with huge amount of data. We limit " "it to 3 lines, each with 10 million points, so that it uses less than 1G of RAM " "(320M to store the data, 600M to plot the data and handle the GUI)." "<*br*><*br*><*br*>Press the Plot Chart button to plot the chart."); m_ChartViewer.setChart(c); return TRUE; } // // User clicks on the Pointer pushbutton // void CMegaZoomScrollDlg::OnPointerPB() { m_ChartViewer.setMouseUsage(Chart::MouseUsageScroll); } // // User clicks on the Zoom In pushbutton // void CMegaZoomScrollDlg::OnZoomInPB() { m_ChartViewer.setMouseUsage(Chart::MouseUsageZoomIn); } // // User clicks on the Zoom Out pushbutton // void CMegaZoomScrollDlg::OnZoomOutPB() { m_ChartViewer.setMouseUsage(Chart::MouseUsageZoomOut); } // // 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 // CChartViewer.updateViewPort. // void CMegaZoomScrollDlg::OnViewPortChanged() { if (!m_hasFinishedInitialization) return; // 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()) drawChart(&m_ChartViewer); } // // User clicks on the the horizontal scroll bar // void CMegaZoomScrollDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { double newViewPortLeft = moveScrollBar(nSBCode, nPos, pScrollBar); // Update the view port if the scroll bar has really moved if (m_hasFinishedInitialization && (newViewPortLeft != m_ChartViewer.getViewPortLeft())) { m_ChartViewer.setViewPortLeft(newViewPortLeft); m_ChartViewer.updateViewPort(true, false); } } // // Draw track cursor when mouse is moving over plotarea // void CMegaZoomScrollDlg::OnMouseMovePlotArea() { if (!m_hasFinishedInitialization) return; trackLineLabel((XYChart*)m_ChartViewer.getChart(), m_ChartViewer.getPlotAreaMouseX()); m_ChartViewer.updateDisplay(); } namespace //anonymouse namespace { // // ThreadTask - A utility to run code in separate threads. // // Usage: // - Derived from ThreadTask, and override the "virtual void run()" method. Put the // code you want to run in this method. // // - Create your derived object, and call the "start()" method. The object will then // execute the run() method in another thread. The "start()" method will return // immediately without blocking. In this way, you can create multiple objects to // run code in separate threads. // // - You can call the "wait()" method if you want to wait until the code in "run()" // has been completed. // class ThreadTask { private: //disable copying ThreadTask(const ThreadTask& rhs); ThreadTask& operator=(const ThreadTask& rhs); HANDLE doneEvent; static DWORD WINAPI threadHandler(_In_ LPVOID lpParameter) { ThreadTask* self = (ThreadTask*)lpParameter; self->run(); SetEvent(self->doneEvent); return 0; } public: ThreadTask() : doneEvent(0) { } virtual ~ThreadTask() { if (doneEvent) CloseHandle(doneEvent); } void start() { doneEvent = CreateEventA(NULL, FALSE, FALSE, NULL); QueueUserWorkItem(threadHandler, this, 0); } DWORD wait(DWORD timeout = INFINITE) { return doneEvent ? WaitForSingleObject(doneEvent, timeout) : WAIT_FAILED; } virtual void run() = 0; }; // A thread task used for random number generator. Because of the large number of data // points, we will create 3 random number generators running in 3 threads to speed up // random number generation. class RanSeriesTask : public ThreadTask { private: int seed; // random number seed double* buffer; // buffer to store the result int count; // the count of random numbers public: RanSeriesTask(int seed, double* buffer, int count) : seed(seed), buffer(buffer), count(count) { start(); // auto start the thread after object creation } virtual void run() { // Generate random numbers RanSeries r(seed); r.fillSeries(buffer, count, 2500, -1, 1); } }; // A thread task used for processing the data series in the data accelerator. We will // create 3 threads so that 3 data series can be processed concurrently. class FastSeriesTask : public ThreadTask { private: DataAccelerator* fastDB; const char* id; const double* data; int len; public: FastSeriesTask(DataAccelerator* fastDB, const char* id, const double* data, int len) : fastDB(fastDB), id(id), data(data), len(len) { start(); // auto start the thread after object creation } virtual void run() { // Process the data series fastDB->addDataSeries(id, data, len); } }; } // // Load the data // void CMegaZoomScrollDlg::loadData() { // Allocate space for the data arrays m_dataSeriesA.resize(bufferSize); m_dataSeriesB.resize(bufferSize); m_dataSeriesC.resize(bufferSize); m_timeStamps.resize(bufferSize); // To speed up random number generation, we use 3 threads to generate the random data // for the 3 data series. The current thread is used for generating the timestamps. RanSeriesTask taskA(109, &m_dataSeriesA[0], (int)m_dataSeriesA.size()); RanSeriesTask taskB(110, &m_dataSeriesB[0], (int)m_dataSeriesB.size()); RanSeriesTask taskC(111, &m_dataSeriesC[0], (int)m_dataSeriesC.size()); for (int i = 0; i < (int)m_timeStamps.size(); ++i) m_timeStamps[i] = i; taskA.wait(); taskB.wait(); taskC.wait(); } // // User clicks on the Plot Chart pushbutton // void CMegaZoomScrollDlg::OnClickPlotChart() { // Has already initialized ?? if (m_hasFinishedInitialization) return; // Use DataAccerlerate the accelerate rendering. To speed up, we create two threads to // process two of the data series, and use the current thread to process the third series. m_fastData = new DataAccelerator(&m_timeStamps[0], (int)m_timeStamps.size()); FastSeriesTask task1(m_fastData, "mA", &m_dataSeriesA[0], (int)m_dataSeriesA.size()); FastSeriesTask task2(m_fastData, "mB", &m_dataSeriesB[0], (int)m_dataSeriesB.size()); m_fastData->addDataSeries("mC", &m_dataSeriesC[0], (int)m_dataSeriesC.size()); task1.wait(); task2.wait(); // Initialize the CChartViewer initChartViewer(&m_ChartViewer); m_hasFinishedInitialization = true; // Trigger the ViewPortChanged event to draw the chart m_ChartViewer.updateViewPort(true, true); } // // Initialize the CChartViewer // void CMegaZoomScrollDlg::initChartViewer(CChartViewer* viewer) { // Set the full x range to be the duration of the data viewer->setFullRange("x", m_timeStamps[0], m_timeStamps[m_timeStamps.size() - 1]); // Initialize the view port to show the latest 20% of the time range viewer->setViewPortWidth(0.2); viewer->setViewPortLeft(1 - viewer->getViewPortWidth()); // Set the maximum zoom to 10 points viewer->setZoomInWidthLimit(10.0 / m_timeStamps.size()); // Enable mouse wheel zooming by setting the zoom ratio to 1.1 per wheel event viewer->setMouseWheelZoomRatio(1.1); // Initially set the mouse to drag to scroll mode. m_PointerPB.SetCheck(1); viewer->setMouseUsage(Chart::MouseUsageScroll); } // // Update other controls that may be affected by the viewport. // void CMegaZoomScrollDlg::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); } } // // Handle scroll bar events // double CMegaZoomScrollDlg::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); } // // Draw the chart and display it in the given viewer // void CMegaZoomScrollDlg::drawChart(CChartViewer* viewer) { // 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()); m_fastData->setSubsetRange(viewPortStartDate, viewPortEndDate); // // At this stage, we have extracted the visible data. We can use those data to plot the chart. // //================================================================================ // Configure overall chart appearance. //================================================================================ XYChart* c = new XYChart(800, 400); // Set the plotarea at (0, 0) with width 1 pixel 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(0, 0, c->getWidth() - 1, c->getHeight() - 20, c->linearGradientColor(0, 0, 0, c->getHeight() - 20, 0xf0f6ff, 0xa0c0ff), -1, Chart::Transparent, 0xffffff, 0xffffff); // In our code, we can overdraw the line slightly, so we clip it to the plot area. c->setClipping(); // Add a legend box at the right side using horizontal layout. Use 10pt Arial Bold as font. Set // the background and border color to Transparent and use line style legend key. LegendBox* b = c->addLegend(c->getWidth() - 1, 10, false, "Arial Bold", 10); b->setBackground(Chart::Transparent); b->setAlignment(Chart::Right); 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, 0x336699); // Configure the y-axis label to be inside the plot area and above the horizontal grid lines c->yAxis()->setLabelGap(-1); c->yAxis()->setMargin(20); c->yAxis()->setLabelAlignment(1); // Configure the x-axis labels to be to the left of the vertical grid lines c->xAxis()->setLabelAlignment(1); //================================================================================ // Add data to chart //================================================================================ // Add line layers using the DataAccelerator. Each layer only supports one accelerated // series, so we add 3 layers for the 3 data series. LineLayer* layer = c->addLineLayer(m_fastData, "mA", 0xff0000, "Alpha"); layer->setLineWidth(2); LineLayer* layer2 = c->addLineLayer(m_fastData, "mB", 0x00cc00, "Beta"); layer2->setLineWidth(2); LineLayer* layer3 = c->addLineLayer(m_fastData, "mC", 0x0000ff, "Gamma"); layer3->setLineWidth(2); //================================================================================ // Configure axis scale and labelling //================================================================================ // Set the x-axis as a date/time axis with the scale according to the view port x range. viewer->syncLinearAxisWithViewPort("x", c->xAxis()); // For the automatic axis labels, set the minimum spacing to 75/40 pixels for the x/y axis. c->xAxis()->setTickDensity(75); c->yAxis()->setTickDensity(40); // Set the auto-scale margin to 0.05, and the zero affinity to 0.2 c->yAxis()->setAutoScale(0.05, 0.05, 0.2); //================================================================================ // 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()) && viewer->isMouseOnPlotArea()) trackLineLabel(c, viewer->getPlotAreaMouseX()); delete viewer->getChart(); viewer->setChart(c); } // // Draw the track line with legend // void CMegaZoomScrollDlg::trackLineLabel(XYChart* c, int mouseX) { // Obtain the dynamic layer of the chart 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. std::ostringstream xlabel; xlabel << "<*font,bgColor=000000*> " << c->formatValue(xValue, "{value}") << " <*/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() + 2, 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, name 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, 4, 4, color, color); std::ostringstream label; label << "<*font,bgColor=" << std::hex << color << "*> " << c->formatValue(dataSet->getValue(xIndex), "{value|P4}") << " <*font*>"; 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, 0xffffff, Chart::Left); else t->draw(xCoor - 6, yCoor, 0xffffff, Chart::Right); t->destroy(); } } } } ///////////////////////////////////////////////////////////////////////////// // General utilities // // Load an icon resource into a button // void CMegaZoomScrollDlg::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)); }