ChartDirector 5.1 (C++ Edition)

XY Zooming and Scrolling (MFC)




NOTE: This section describes XY Zooming and Scrolling for MFC, and is available on the Windows edition of ChartDirector for C++ only. For QT, please refer to XY Zooming and Scrolling (QT).

This example demonstrates zooming and scrolling in both horizontal and vertical directions. In addition to using mouse click and drag, this example demonstrates using a slider and the mouse wheel to control zooming. There is also a "navigation pad" which can be thought as a representation of the view port. You can drag the pad to move the view port around (that is, to perform 2D scrolling). This example also includes a crosshair track cursor with dynamic labels on the x-axis and y-axis showing the mouse cursor position, and an image map for data point tooltips.

The main source code listing of this sample program is included at the end of this section. The code consists of the following main parts:

Source Code Listing

The following is the main source code of this demo. The complete MFC project is in "mfcdemo/xyzoomscroll".

[File: mfcdemo/xyzoomscroll/xyzoomscrollDlg.cpp]
// xyzoomscrollDlg.cpp : implementation file
//

#include "stdafx.h"
#include "xyzoomscroll.h"
#include "xyzoomscrollDlg.h"
#include "HotSpotDlg.h"
#include "chartdir.h"
#include <sstream>
#include <algorithm>

using namespace std;

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

/////////////////////////////////////////////////////////////////////////////
// CXyzoomscrollDlg dialog

//
// Constructor
//
CXyzoomscrollDlg::CXyzoomscrollDlg(CWnd* pParent /*=NULL*/)
    : CDialog(CXyzoomscrollDlg::IDD, pParent)
{
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

//
// Destructor
//
CXyzoomscrollDlg::~CXyzoomscrollDlg()
{
    delete m_ChartViewer.getChart();
}


void CXyzoomscrollDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CXyzoomscrollDlg)
    DDX_Control(pDX, IDC_PointerPB, m_PointerPB);
    DDX_Control(pDX, IDC_NavigatePad, m_NavigatePad);
    DDX_Control(pDX, IDC_NavigateWindow, m_NavigateWindow);
    DDX_Control(pDX, IDC_ZoomBar, m_ZoomBar);
    DDX_Control(pDX, IDC_ChartViewer, m_ChartViewer);
    //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CXyzoomscrollDlg, CDialog)
    //{{AFX_MSG_MAP(CXyzoomscrollDlg)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(IDC_PointerPB, OnPointerPB)
    ON_BN_CLICKED(IDC_ZoomInPB, OnZoomInPB)
    ON_BN_CLICKED(IDC_ZoomOutPB, OnZoomOutPB)
    ON_WM_HSCROLL()
    ON_WM_MOUSEWHEEL()
    ON_BN_CLICKED(IDC_NavigateWindow, OnNavigateWindow)
    ON_BN_CLICKED(IDC_ChartViewer, OnChartViewer)
    ON_CONTROL(CVN_ViewPortChanged, IDC_ChartViewer, OnViewPortChanged)
    ON_CONTROL(CVN_MouseMovePlotArea, IDC_ChartViewer, OnMouseMovePlotArea)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CXyzoomscrollDlg message handlers

//
// Initialization
//
BOOL CXyzoomscrollDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // *** code automatically generated by VC++ MFC AppWizard ***
    // Set the icon for this dialog.  The framework does this automatically
    // when the application's main window is not a dialog
    SetIcon(m_hIcon, TRUE);         // Set big icon
    SetIcon(m_hIcon, FALSE);        // Set small icon
    
    //
    // Initialize member variables
    //
    m_extBgColor = getDefaultBgColor();     // Default background color

    //
    // Initialize controls
    //

    // Load icons to mouse usage buttons
    loadButtonIcon(IDC_PointerPB, IDI_PointerPB, 100, 20);  
    loadButtonIcon(IDC_ZoomInPB, IDI_ZoomInPB, 100, 20);    
    loadButtonIcon(IDC_ZoomOutPB, IDI_ZoomOutPB, 100, 20);  

    // Set Pointer pushbutton into clicked state
    m_PointerPB.SetCheck(1);

    // Initialize zoom bar
    m_ZoomBar.SetRange(1, 100);
    m_ZoomBar.SetPageSize(5);
    m_ZoomBar.SetTicFreq(10);

    // Set initial mouse usage for ChartViewer
    m_ChartViewer.setMouseUsage(Chart::MouseUsageScroll);
    m_ChartViewer.setScrollDirection(Chart::DirectionHorizontalVertical);
    m_ChartViewer.setZoomDirection(Chart::DirectionHorizontalVertical);

    // Display the chart
    m_ChartViewer.updateViewPort(true, true);
    return TRUE;
}

// *** code automatically generated by VC++ MFC AppWizard ***
// If you add a minimize button to your dialog, you will need the code below
// to draw the icon.  For MFC applications using the document/view model,
// this is automatically done for you by the framework.
void CXyzoomscrollDlg::OnPaint() 
{
    if (IsIconic())
    {
        CPaintDC dc(this); // device context for painting

        SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);

        // Center icon in client rectangle
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
        GetClientRect(&rect);
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;

        // Draw the icon
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        CDialog::OnPaint();
    }
}

// *** code automatically generated by VC++ MFC AppWizard ***
// The system calls this to obtain the cursor to display while the user drags
// the minimized window.
HCURSOR CXyzoomscrollDlg::OnQueryDragIcon()
{
    return (HCURSOR) m_hIcon;
}

//
// User clicks on the Pointer pushbutton
//
void CXyzoomscrollDlg::OnPointerPB() 
{
    m_ChartViewer.setMouseUsage(Chart::MouseUsageScroll);   
}

//
// User clicks on the Zoom In pushbutton
//
void CXyzoomscrollDlg::OnZoomInPB() 
{
    m_ChartViewer.setMouseUsage(Chart::MouseUsageZoomIn);   
}

//
// User clicks on the Zoom Out pushbutton
//
void CXyzoomscrollDlg::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 CXyzoomscrollDlg::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())
        drawChart(&m_ChartViewer);
    
    // Update the image map if necessary
    if (m_ChartViewer.needUpdateImageMap())
        updateImageMap(&m_ChartViewer);

    // 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 ((!m_ChartViewer.isInMouseMoveEvent()) && m_ChartViewer.isMouseOnPlotArea())
    {
        crossHair((XYChart *)m_ChartViewer.getChart(), m_ChartViewer.getPlotAreaMouseX(), 
            m_ChartViewer.getPlotAreaMouseY());
        m_ChartViewer.updateDisplay();
    }
}

//
// The mouse wheel handler
//
BOOL CXyzoomscrollDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
    // Process the mouse wheel only if the mouse is over the plot area
    if (!m_ChartViewer.isMouseOnPlotArea())
        return FALSE;

    // We zoom in or out by 10% depending on the mouse wheel direction.
    double newVpWidth = m_ChartViewer.getViewPortWidth() * (zDelta > 0 ? 0.9 : 1 / 0.9);
    double newVpHeight = m_ChartViewer.getViewPortHeight() * (zDelta > 0 ? 0.9 : 1 / 0.9);

    // We do not zoom beyond the zoom width or height limits.
    newVpWidth = max(m_ChartViewer.getZoomInWidthLimit(), min(newVpWidth,
        m_ChartViewer.getZoomOutWidthLimit()));
    newVpHeight = max(m_ChartViewer.getZoomInHeightLimit(), min(newVpWidth,
        m_ChartViewer.getZoomOutHeightLimit()));
    
    if ((newVpWidth != m_ChartViewer.getViewPortWidth()) || 
        (newVpHeight != m_ChartViewer.getViewPortHeight()))
    {
        // Set the view port position and size so that the point under the mouse remains under
        // the mouse after zooming.

        double deltaX = (m_ChartViewer.getPlotAreaMouseX() - m_ChartViewer.getPlotAreaLeft()) * 
            (m_ChartViewer.getViewPortWidth() - newVpWidth) / m_ChartViewer.getPlotAreaWidth();
        m_ChartViewer.setViewPortLeft(m_ChartViewer.getViewPortLeft() + deltaX);
        m_ChartViewer.setViewPortWidth(newVpWidth);

        double deltaY = (m_ChartViewer.getPlotAreaMouseY() - m_ChartViewer.getPlotAreaTop()) *
            (m_ChartViewer.getViewPortHeight() - newVpHeight) / m_ChartViewer.getPlotAreaHeight();
        m_ChartViewer.setViewPortTop(m_ChartViewer.getViewPortTop() + deltaY);
        m_ChartViewer.setViewPortHeight(newVpHeight);

        m_ChartViewer.updateViewPort(true, false);
    }

    return TRUE;
}

//
// Draw track cursor when mouse is moving over plotarea, and update image map if necessary
//
void CXyzoomscrollDlg::OnMouseMovePlotArea()
{
    // Get the focus to ensure being able to receive mouse wheel events
    m_ChartViewer.SetFocus();

    // Draw crosshair track cursor
    crossHair((XYChart *)m_ChartViewer.getChart(), m_ChartViewer.getPlotAreaMouseX(), 
        m_ChartViewer.getPlotAreaMouseY());
    m_ChartViewer.updateDisplay();

    // Hide the track cursor when the mouse leaves the plot area
    m_ChartViewer.removeDynamicLayer(CVN_MouseLeavePlotArea);

    // Update image map if necessary
    updateImageMap(&m_ChartViewer);
}

//
// User moves the Zoom slider control
//
void CXyzoomscrollDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 
{
    if (nSBCode != SB_ENDSCROLL)
    {
        //
        // The slider is moving. Update the chart.
        //

        // Remember the center point
        double centerX = m_ChartViewer.getViewPortLeft() + 
            m_ChartViewer.getViewPortWidth() / 2;
        double centerY = m_ChartViewer.getViewPortTop() + 
            m_ChartViewer.getViewPortHeight() / 2;
            
        // Aspect ratio and zoom factor
        double aspectRatio = m_ChartViewer.getViewPortWidth() / 
            m_ChartViewer.getViewPortHeight();
        double zoomTo = ((double)m_ZoomBar.GetPos()) / m_ZoomBar.GetRangeMax();

        // Zoom by adjusting ViewPortWidth and ViewPortHeight while maintaining the aspect 
        // ratio
        m_ChartViewer.setViewPortWidth(zoomTo * ((aspectRatio < 1) ? 1 : aspectRatio));
        m_ChartViewer.setViewPortHeight(zoomTo * ((aspectRatio > 1) ? 1 : (1 / aspectRatio)));
        
        // Adjust ViewPortLeft and ViewPortTop to keep center point unchanged
        m_ChartViewer.setViewPortLeft(centerX - m_ChartViewer.getViewPortWidth() / 2);
        m_ChartViewer.setViewPortTop(centerY - m_ChartViewer.getViewPortHeight() / 2);
        
        // Update the chart image only, but no need to update the image map.
        m_ChartViewer.updateViewPort(true, false);
    }
        
    CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}

//
// User drags the navigate window
//
void CXyzoomscrollDlg::OnNavigateWindow() 
{
    if (m_NavigateWindow.IsMouseDrag())
    {
        //
        // Get the position of the navigate window inside the navigate pad as a ratio between 
        // 0 - 1. (Note: we allowed for a 2-pixel margin for the frame border in the following
        // computations.)
        //

        CRect windowPos;
        m_NavigateWindow.GetWindowRect(&windowPos);

        CRect containerPos;
        m_NavigatePad.GetWindowRect(&containerPos);

        double viewPortLeft = ((double)windowPos.left + m_NavigateWindow.GetMouseDragPos().x 
            - m_NavigateWindow.GetMouseDownPos().x - containerPos.left - 2) / 
            (containerPos.Width() - 4);
        double viewPortTop = ((double)windowPos.top + m_NavigateWindow.GetMouseDragPos().y
            - m_NavigateWindow.GetMouseDownPos().y - containerPos.top - 2) / 
            (containerPos.Height() - 4);
        
        //
        // Ensures the view port is within valid range.
        //
        viewPortLeft = max(0.0, min(viewPortLeft, 1 - m_ChartViewer.getViewPortWidth()));
        viewPortTop = max(0.0, min(viewPortTop, 1 - m_ChartViewer.getViewPortHeight()));

        //
        // Update the chart view port
        //
        m_ChartViewer.setViewPortLeft(viewPortLeft);
        m_ChartViewer.setViewPortTop(viewPortTop);

        // Update the chart image only, but no need to update the image map.
        m_ChartViewer.updateViewPort(true, false);
    }
}

//
// User clicks on the CChartViewer
//
void CXyzoomscrollDlg::OnChartViewer() 
{
    ImageMapHandler *handler = m_ChartViewer.getImageMapHandler();
    if (0 != handler)
    {
        //
        // Query the ImageMapHandler to see if the mouse is on a clickable hot spot. We 
        // consider the hot spot as clickable if its href ("path") parameter is not empty.
        //
        const char *path = handler->getValue("path");
        if ((0 != path) && (0 != *path))
        {
            // In this sample code, we just show all hot spot parameters.
            CHotSpotDlg hs;
            hs.SetData(handler);
            hs.DoModal();
        }
    }
}

/////////////////////////////////////////////////////////////////////////////
// CXyzoomscrollDlg methods

//
// Draw the chart and display it in the given viewer
//
void CXyzoomscrollDlg::drawChart(CChartViewer *viewer)
{
    //
    // For simplicity, in this demo, we just use hard coded data. In a real application,
    // the data probably read from a dynamic source such as a database. (See the
    // ChartDirector documentation on "Using Data Sources with ChartDirector" if you need
    // some sample code on how to read data from database to array variables.)
    //
    double dataX0[] = {10, 15, 6, -12, 14, -8, 13, -3, 16, 12, 10.5, -7, 3, -10, -5, 2, 5};
    double dataY0[] = {130, 150, 80, 110, -110, -105, -130, -15, -170, 125,  125, 60, 25, 150,
        150,15, 120};
    double dataX1[] = {6, 7, -4, 3.5, 7, 8, -9, -10, -12, 11, 8, -3, -2, 8, 4, -15, 15};
    double dataY1[] = {65, -40, -40, 45, -70, -80, 80, 10, -100, 105, 60, 50, 20, 170, -25, 
        50, 75};
    double dataX2[] = {-10, -12, 11, 8, 6, 12, -4, 3.5, 7, 8, -9, 3, -13, 16, -7.5, -10, -15};
    double dataY2[] = {65, -80, -40, 45, -70, -80, 80, 90, -100, 105, 60, -75, -150, -40, 120,
        -50, -30};
                
    // Create an XYChart object 500 x 480 pixels in size, with the same background color
    // as the container
    XYChart *c = new XYChart(500, 480, m_extBgColor);

    // Set the plotarea at (50, 40) and of size 400 x 400 pixels. Use light grey (c0c0c0)
    // horizontal and vertical grid lines. Set 4 quadrant coloring, where the colors of 
    // the quadrants alternate between lighter and deeper grey (dddddd/eeeeee). 
    c->setPlotArea(50, 40, 400, 400, -1, -1, -1, 0xc0c0c0, 0xc0c0c0
        )->set4QBgColor(0xdddddd, 0xeeeeee, 0xdddddd, 0xeeeeee, 0x000000);

    // Enable clipping mode to clip the part of the data that is outside the plot area.
    c->setClipping();

    // Set 4 quadrant mode, with both x and y axes symetrical around the origin
    c->setAxisAtOrigin(Chart::XYAxisAtOrigin, Chart::XAxisSymmetric + Chart::YAxisSymmetric);

    // Add a legend box at (450, 40) (top right corner of the chart) with vertical layout
    // and 8 pts Arial Bold font. Set the background color to semi-transparent grey.
    LegendBox *legendBox = c->addLegend(450, 40, true, "arialbd.ttf", 8);
    legendBox->setAlignment(Chart::TopRight);
    legendBox->setBackground(0x40dddddd);

    // Add a titles to axes
    c->xAxis()->setTitle("Alpha Index");
    c->yAxis()->setTitle("Beta Index");

    // Set axes width to 2 pixels
    c->xAxis()->setWidth(2);
    c->yAxis()->setWidth(2);

    // The default ChartDirector settings has a denser y-axis grid spacing and less-dense
    // x-axis grid spacing. In this demo, we want the tick spacing to be symmetrical.
    // We use around 50 pixels between major ticks and 25 pixels between minor ticks.
    c->xAxis()->setTickDensity(50, 25);
    c->yAxis()->setTickDensity(50, 25);

    //
    // In this example, we represent the data by scatter points. If you want to represent
    // the data by somethings else (lines, bars, areas, floating boxes, etc), just modify
    // the code below to use the layer type of your choice. 
    //

    // Add scatter layer, using 11 pixels red (ff33333) X shape symbols
    c->addScatterLayer(DoubleArray(dataX0, sizeof(dataX0) / sizeof(dataX0[0])), 
        DoubleArray(dataY0, sizeof(dataY0) / sizeof(dataY0[0])), "Group A", 
        Chart::Cross2Shape(), 11, 0xff3333);

    // Add scatter layer, using 11 pixels green (33ff33) circle symbols
    c->addScatterLayer(DoubleArray(dataX1, sizeof(dataX1) / sizeof(dataX1[0])),
        DoubleArray(dataY1, sizeof(dataY1) / sizeof(dataY1[0])), 
        "Group B", Chart::CircleShape, 11,  0x33ff33);

    // Add scatter layer, using 11 pixels blue (3333ff) triangle symbols
    c->addScatterLayer(DoubleArray(dataX2, sizeof(dataX2) / sizeof(dataX2[0])),
        DoubleArray(dataY2, sizeof(dataY2) / sizeof(dataY2[0])), 
        "Group C", Chart::TriangleSymbol, 11, 0x3333ff);

    //
    // In this example, we have not explicitly configured the full x and y range. In this case, 
    // the first time syncLinearAxisWithViewPort is called, ChartDirector will auto-scale the axis
    // and assume the resulting range is the full range. In subsequent calls, ChartDirector will 
    // set the axis range based on the view port and the full range.
    //
    viewer->syncLinearAxisWithViewPort("x", c->xAxis());
    viewer->syncLinearAxisWithViewPort("y", c->yAxis());
    
    // Set the chart image to the WinChartViewer
    delete viewer->getChart();
    viewer->setChart(c);
}

//
// Draw cross hair cursor with axis labels
//
void CXyzoomscrollDlg::crossHair(XYChart *c, int mouseX, int mouseY)
{
    // 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();

    // Draw a vertical line and a horizontal line as the cross hair
    d->vline(plotArea->getTopY(), plotArea->getBottomY(), mouseX, d->dashLineColor(0x000000, 0x0101));
    d->hline(plotArea->getLeftX(), plotArea->getRightX(), mouseY, d->dashLineColor(0x000000, 0x0101));

    // Draw y-axis label
    ostringstream ylabel;
    ylabel << "<*block,bgColor=FFFFDD,margin=3,edgeColor=000000*>" << c->formatValue(c->getYValue(
        mouseY, c->yAxis()), "{value|P4}") << "<*/*>";
    TTFText *t = d->text(ylabel.str().c_str(), "arialbd.ttf", 8);
    t->draw(plotArea->getLeftX() - 5, mouseY, 0x000000, Chart::Right);
    t->destroy();

    // Draw x-axis label
    ostringstream xlabel;
    xlabel << "<*block,bgColor=FFFFDD,margin=3,edgeColor=000000*>" << c->formatValue(c->getXValue(
        mouseX), "{value|P4}") << "<*/*>";
    t = d->text(xlabel.str().c_str(), "arialbd.ttf", 8);
    t->draw(mouseX, plotArea->getBottomY() + 5, 0x000000, Chart::Top);
    t->destroy();
}

//
// Update the image map
//
void CXyzoomscrollDlg::updateImageMap(CChartViewer *viewer)
{
    if (0 == viewer->getImageMapHandler())
    {
        // no existing image map - creates a new one
        viewer->setImageMap(viewer->getChart()->getHTMLImageMap("clickable", "",
                "title='[{dataSetName}] Alpha = {x}, Beta = {value}'"));
    }
}

//
// Update the image map
//
void CXyzoomscrollDlg::updateControls(CChartViewer *viewer)
{
    //
    // Update the Zoom slider to reflect the current zoom level of the view port
    //
    double smallerSide = viewer->getViewPortWidth() > viewer->getViewPortHeight() 
        ? viewer->getViewPortHeight() : viewer->getViewPortWidth();
    m_ZoomBar.SetPos((int)(smallerSide * m_ZoomBar.GetRangeMax() + 0.5));

    //
    // Update the navigate window to reflect the current view port position and size. (Note: 
    // we allowed for a 2-pixel margin for the frame border in the following computations.)
    //
    CRect container;
    m_NavigatePad.GetWindowRect(&container);
    ScreenToClient(&container);
    
    int left = (int)(viewer->getViewPortLeft() * (container.Width() - 4) + 2 + 0.5);
    int top = (int)(viewer->getViewPortTop() * (container.Height() - 4) + 2 + 0.5);
    int width = (int)(viewer->getViewPortWidth() * (container.Width() - 4) + 0.5);
    int height = (int)(viewer->getViewPortHeight() * (container.Height() - 4) + 0.5);
    m_NavigateWindow.MoveWindow(left + container.left, container.top + top, width, height);    
}


/////////////////////////////////////////////////////////////////////////////
// General utilities

//
// Get the default background color
//
int CXyzoomscrollDlg::getDefaultBgColor()
{
    LOGBRUSH LogBrush; 
    HBRUSH hBrush = (HBRUSH)SendMessage(WM_CTLCOLORDLG, (WPARAM)CClientDC(this).m_hDC, 
        (LPARAM)m_hWnd); 
    ::GetObject(hBrush, sizeof(LOGBRUSH), &LogBrush); 
    int ret = LogBrush.lbColor;
    return ((ret & 0xff) << 16) | (ret & 0xff00) | ((ret & 0xff0000) >> 16);
}

//
// Load an icon resource into a button
//
void CXyzoomscrollDlg::loadButtonIcon(int buttonId, int iconId, int width, int height)
{
    GetDlgItem(buttonId)->SendMessage(BM_SETIMAGE, IMAGE_ICON, (LPARAM)::LoadImage(
        AfxGetResourceHandle(), MAKEINTRESOURCE(iconId), IMAGE_ICON, width, height, 
        LR_DEFAULTCOLOR));  
}