ChartDirector Ver 4.1 (C++ Edition)

Zooming and Scrolling Demonstration




This sample program demonstrates a zoomable and scrollable chart for time based data series. The followings are the key elements of this sample program:

This sample program is a Windows program written using MFC, and is available on the Windows edition of ChartDirector for C++ only.

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/zoomscrolldemo".

[File: mfcdemo/zoomscrolldemo/zoomscrolldemoDlg.cpp]
// zoomscrolldemoDlg.cpp : implementation file
//

#include "stdafx.h"
#include "zoomscrolldemo.h"
#include "zoomscrolldemoDlg.h"
#include "HotSpotDlg.h"
#include "chartdir.h"
#include <math.h>
#include <time.h>
#include <algorithm>

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CZoomscrolldemoDlg dialog

//
// Constructor
//
CZoomscrolldemoDlg::CZoomscrolldemoDlg(CWnd* pParent /*=NULL*/)
    : CDialog(CZoomscrolldemoDlg::IDD, pParent)
{
    //{{AFX_DATA_INIT(CZoomscrolldemoDlg)
    //}}AFX_DATA_INIT
    // Note that LoadIcon does not require a subsequent DestroyIcon in Win32
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

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

void CZoomscrolldemoDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CZoomscrolldemoDlg)
    DDX_Control(pDX, IDC_StartDate, m_StartDate);
    DDX_Control(pDX, IDC_Duration, m_Duration);
    DDX_Control(pDX, IDC_VScrollBar, m_VScrollBar);
    DDX_Control(pDX, IDC_HScrollBar, m_HScrollBar);
    DDX_Control(pDX, IDC_XZoomPB, m_XZoomPB);
    DDX_Control(pDX, IDC_PointerPB, m_PointerPB);
    DDX_Control(pDX, IDC_ChartViewer, m_ChartViewer);
    //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CZoomscrolldemoDlg, CDialog)
    //{{AFX_MSG_MAP(CZoomscrolldemoDlg)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_WM_DESTROY()
    ON_BN_CLICKED(IDC_PointerPB, OnPointerPB)
    ON_BN_CLICKED(IDC_ZoomInPB, OnZoomInPB)
    ON_BN_CLICKED(IDC_ZoomOutPB, OnZoomOutPB)
    ON_BN_CLICKED(IDC_XZoomPB, OnXZoomPB)
    ON_BN_CLICKED(IDC_XYZoomPB, OnXYZoomPB)
    ON_BN_CLICKED(IDC_ChartViewer, OnChartViewer)
    ON_CONTROL(CVN_ViewPortChanged, IDC_ChartViewer, OnViewPortChanged)
    ON_WM_HSCROLL()
    ON_WM_VSCROLL()
    ON_NOTIFY(DTN_DATETIMECHANGE, IDC_StartDate, OnDatetimechangeStartDate)
    ON_CBN_SELCHANGE(IDC_Duration, OnSelchangeDuration)
    ON_CBN_KILLFOCUS(IDC_Duration, OnKillfocusDuration)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CZoomscrolldemoDlg message handlers

//
// Initialization
//
BOOL CZoomscrolldemoDlg::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
    
    // 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);

    // Load icons to zoom/scroll direction control buttons
    loadButtonIcon(IDC_XZoomPB, IDI_XZoomPB, 105, 20);
    loadButtonIcon(IDC_XYZoomPB, IDI_XYZoomPB, 105, 20);

    //
    // Initialize member variables
    //
    m_extBgColor = getDefaultBgColor();     // Default background color
    m_minValue = m_maxValue = 0;            // y axes ranges

    // Load the data
    loadData();

    //
    // In this demo, we deduce the horizontal scroll range from the data.
    //

    // Earliest date is the first timestamp
    m_minDate = m_timeStamps[0];
    // Duration is the seconds elapsed of the last timestamp from the earliest date
    m_dateRange = m_timeStamps[m_timeStamps.len - 1] - m_minDate;

    // Convert the start time (in chartTime format) as an MFC CTime value
    int startYMD = Chart::getChartYMD(m_timeStamps[0]);
    int startHMS = (int)fmod(m_timeStamps[0], 86400);
    CTime startDate = CTime(startYMD / 10000, (startYMD % 10000) / 100, startYMD % 100,
        startHMS / 3600, (startHMS % 3600) / 60, startHMS % 60);

    // Convert the end time (in chartTime format) as an MFC CTime value
    int endYMD = Chart::getChartYMD(m_timeStamps[m_timeStamps.len - 1]);
    int endHMS = (int)fmod(m_timeStamps[m_timeStamps.len - 1], 86400);
    CTime endDate = CTime(endYMD / 10000, (endYMD % 10000) / 100, endYMD % 100,
        endHMS / 3600, (endHMS % 3600) / 60, endHMS % 60);
    
    // Set the startDate and endDate to the CDateTimeCtrl control 
    m_StartDate.SetRange(&startDate, &endDate);

    // In this demo, the maximum zoom-in is set to 10 days (1 day = 86400 seconds)
    m_minDuration = 10 * 86400;

    // Set up the ChartViewer to reflect the visible and minimum duration
    m_ChartViewer.setZoomInWidthLimit(m_minDuration / m_dateRange);
    m_ChartViewer.setViewPortWidth(m_currentDuration / m_dateRange);
    m_ChartViewer.setViewPortLeft(1 - m_ChartViewer.getViewPortWidth());

    // Initially set the mouse to drag to scroll mode in horizontal direction.
    m_PointerPB.SetCheck(1);
    m_XZoomPB.SetCheck(1);
    m_ChartViewer.setMouseUsage(Chart::MouseUsageScroll);

    // Can update chart now
    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 CZoomscrolldemoDlg::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 CZoomscrolldemoDlg::OnQueryDragIcon()
{
    return (HCURSOR) m_hIcon;
}

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

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

//
// User clicks on the Zoom Out pushbutton
//
void CZoomscrolldemoDlg::OnZoomOutPB() 
{
    m_ChartViewer.setMouseUsage(Chart::MouseUsageZoomOut);  
}

//
// User clicks on the X-Zoom pushbutton
//
void CZoomscrolldemoDlg::OnXZoomPB() 
{
    m_ChartViewer.setZoomDirection(Chart::DirectionHorizontal); 
    m_ChartViewer.setScrollDirection(Chart::DirectionHorizontal);

    // Viewport is always unzoomed as y-axis is auto-scaled
    m_ChartViewer.setViewPortTop(0);
    m_ChartViewer.setViewPortHeight(1);
        
    // Update chart to auto-scale axis
    m_ChartViewer.updateViewPort(true, true);
}

//
// User clicks on the XY-Zoom pushbutton
//
void CZoomscrolldemoDlg::OnXYZoomPB() 
{
    m_ChartViewer.setZoomDirection(Chart::DirectionHorizontalVertical); 
    m_ChartViewer.setScrollDirection(Chart::DirectionHorizontalVertical);   
}

//
// User selects a start date from the CDateTimeCtrl control
//
void CZoomscrolldemoDlg::OnDatetimechangeStartDate(NMHDR* pNMHDR, LRESULT* pResult) 
{
    // Get the selected date
    CTime startDate;
    m_StartDate.GetTime(startDate);

    // Compute the new view port position based on the selected date
    m_ChartViewer.setViewPortLeft(
       (Chart::chartTime2((int)startDate.GetTime()) - m_minDate) / m_dateRange);
    m_ChartViewer.updateViewPort(true, true);

    *pResult = 0;
}

//
// User selects a duration from the Duration combo box
//
void CZoomscrolldemoDlg::OnSelchangeDuration() 
{
    // Get the selected duration
    CString text;
    m_Duration.GetLBText(m_Duration.GetCurSel(), text);

    // Validate and update the chart
    validateDuration(text);
}

//
// The Duration combo box lost focus (User may have entered a new duration.)
//
void CZoomscrolldemoDlg::OnKillfocusDuration() 
{
    // Get the duration text
    CString text;
    m_Duration.GetWindowText(text);

    // Validate and update the chart
    validateDuration(text);
}

//
// User presses "Enter" key. (User may have entered a new duration.)
//
void CZoomscrolldemoDlg::OnOK()
{
    // Same processing as OnKillfocusDuration
    OnKillfocusDuration();
}

//
// User clicks on the the horizontal scroll bar 
//
void CZoomscrolldemoDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 
{
    if (nSBCode != SB_ENDSCROLL)
    {
        // User is still scrolling

        // Set the view port based on the scroll bar
        m_ChartViewer.setViewPortLeft(moveScrollBar(nSBCode, nPos, pScrollBar));
        // Update the chart image only, but no need to update the image map.
        m_ChartViewer.updateViewPort(true, false);
    }
    else
        // Scroll bar has stoped moving. Can update image map now.
        m_ChartViewer.updateViewPort(false, true);
}       

//
// User clicks on the the vertical scroll bar 
//
void CZoomscrolldemoDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 
{
    if (nSBCode != SB_ENDSCROLL)
    {
        // User is still scrolling

        // Set the view port based on the scroll bar
        m_ChartViewer.setViewPortTop(moveScrollBar(nSBCode, nPos, pScrollBar));
        // Update the chart image only, but no need to update the image map.
        m_ChartViewer.updateViewPort(true, false);
    }
    else
        // Scroll bar has stoped moving. Can update image map now.
        m_ChartViewer.updateViewPort(false, true);
}

//
// CChartViewer ViewPortChanged event
//
void CZoomscrolldemoDlg::OnViewPortChanged()
{
    //
    // Set up the scroll bars to reflect the current position and size of the view port
    //
    SCROLLINFO info;
    info.cbSize = sizeof(SCROLLINFO);
    info.fMask = SIF_ALL;
    info.nMin = 0;
    info.nMax = 0x1fffffff;
    
    m_HScrollBar.EnableWindow(m_ChartViewer.getViewPortWidth() < 1);
    if (m_ChartViewer.getViewPortWidth() < 1)
    {
        info.nPage = (int)ceil(m_ChartViewer.getViewPortWidth() * (info.nMax - info.nMin));
        info.nPos = (int)(0.5 + m_ChartViewer.getViewPortLeft() * (info.nMax - info.nMin))
            + info.nMin;
        m_HScrollBar.SetScrollInfo(&info);
    }

    m_VScrollBar.EnableWindow(m_ChartViewer.getViewPortHeight() < 1);
    if (m_ChartViewer.getViewPortHeight() < 1)
    {
        info.nPage = (int)ceil(m_ChartViewer.getViewPortHeight() * (info.nMax - info.nMin));
        info.nPos = (int)(0.5 + m_ChartViewer.getViewPortTop() * (info.nMax - info.nMin))
            + info.nMin;
        m_VScrollBar.SetScrollInfo(&info);
    }

    //
    // Set the start date CDateTimeCtrl control and duration combo box to reflect the current
    // position and size of the view port.
    //

    // Compute the start date (in chartTime) and duration (in seconds) of the view port
    double currentStartDate = m_minDate + (int)(0.5 + m_ChartViewer.getViewPortLeft() 
        * m_dateRange);
    m_currentDuration = (int)(0.5 + m_ChartViewer.getViewPortWidth() * m_dateRange);

    // Set the CDateTimeCtrl control to reflect the start date
    int startYMD = Chart::getChartYMD(currentStartDate);
    int startHMS = (int)fmod(currentStartDate, 86400);
    CTime startDate = CTime(startYMD / 10000, (startYMD % 10000) / 100, startYMD % 100,
        startHMS / 3600, (startHMS % 3600) / 60, startHMS % 60);
    m_StartDate.SetTime(&startDate);

    // Set the duration combo box to reflect the duration (in days)
    CString buffer;
    buffer.Format(_T("%d"), (int)(0.5 + m_currentDuration / 86400));
    m_Duration.SetWindowText(buffer);

    //
    // Update chart and image map if necessary
    //
    if (m_ChartViewer.needUpdateChart())
        drawChart(&m_ChartViewer);
    if (m_ChartViewer.needUpdateImageMap())
        updateImageMap(&m_ChartViewer);
}

//
// User clicks on the CChartViewer
//
void CZoomscrolldemoDlg::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();
        }
    }
}

/////////////////////////////////////////////////////////////////////////////
// CZoomscrolldemoDlg methods

//
// Load the data
//
void CZoomscrolldemoDlg::loadData()
{
    // In this demo, we allow scrolling for the last 5 years.
    time_t t = time(0);
    struct tm *ymd = localtime(&t);
    double lastDate = Chart::chartTime(ymd->tm_year + 1900, ymd->tm_mon, ymd->tm_mday);
    double firstDate = Chart::chartTime(ymd->tm_year + 1900 - 5, ymd->tm_mon, ymd->tm_mday);

    // The initial view port is to show 1 year of data.
    m_currentDuration = lastDate - Chart::chartTime(ymd->tm_year + 1900 - 1, ymd->tm_mon, 
        ymd->tm_mday);
    
    //
    // Get the data and stores them in a memory buffer for fast scrolling / zooming. In 
    // this demo, we just use a random number generator. In practice, you may get the data
    // from a database or XML or by other means.
    //

    // Set up random number generator
    m_dataTable = new RanTable(127, 4, (int)((lastDate - firstDate) / 86400) + 1);
    m_dataTable->setDateCol(0, firstDate, 86400);
    m_dataTable->setCol(1, 150, -10, 10);
    m_dataTable->setCol(2, 200, -10, 10);
    m_dataTable->setCol(3, 250, -10, 10);

    // Read random data into the data arrays
    m_timeStamps = m_dataTable->getCol(0);
    m_dataSeriesA = m_dataTable->getCol(1);
    m_dataSeriesB = m_dataTable->getCol(2);
    m_dataSeriesC = m_dataTable->getCol(3);
}

//
// Handle scroll bar events
//
double CZoomscrolldemoDlg::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);
}

//
// Validate the contents of the duration combo box and update ViewPortWidth accordingly
//
void CZoomscrolldemoDlg::validateDuration(const CString &text)
{
    // Parse the duration text
    double newDuration = _tcstod(text, 0) * 86400;

    // Duration too short or not numeric?
    if (newDuration < m_minDuration) 
        newDuration = m_minDuration;

    if (newDuration != m_currentDuration)
    {
        // Set the ViewPortWidth according to the new duration
        m_currentDuration = newDuration;
        m_ChartViewer.setViewPortWidth(newDuration / m_dateRange);
        
        // Update the chart
        m_ChartViewer.updateViewPort(true, true);
    }
}

//
// Draw the chart and display it in the given viewer
//
void CZoomscrolldemoDlg::drawChart(CChartViewer *viewer)
{
    //
    // In this demo, we copy the visible part of the data to a separate buffer for chart
    // plotting. 
    //
    // Note that if you only have a small amount of data (a few hundred data points), it
    // may be easier to just plot all data in any case (so the following copying code is 
    // not needed), and let ChartDirector "clip" the chart to the plot area. 
    //

    // Using ViewPortLeft and ViewPortWidth, get the start and end dates of the view port.
    double viewPortStartDate = m_minDate + (__int64)(viewer->getViewPortLeft() * 
        m_dateRange + 0.5);
    double viewPortEndDate = viewPortStartDate + (__int64)(viewer->getViewPortWidth() * 
        m_dateRange + 0.5);
        
    // Get the starting index of the array using the start date
    int startIndex = std::lower_bound(m_timeStamps.data, m_timeStamps.data + m_timeStamps.len,
        viewPortStartDate) - m_timeStamps.data;
    if ((startIndex > 0) && (m_timeStamps[startIndex] != viewPortStartDate)) 
        --startIndex;
    
    // Get the ending index of the array using the end date
    int endIndex = std::upper_bound(m_timeStamps.data, m_timeStamps.data + m_timeStamps.len, 
        viewPortEndDate) - m_timeStamps.data;

    // Get the length
    int noOfPoints = endIndex - startIndex;

    // We copy the visible data from the main arrays to separate data arrays
    double* viewPortTimeStamps = new double[noOfPoints];
    double* viewPortDataSeriesA = new double[noOfPoints];
    double* viewPortDataSeriesB = new double[noOfPoints];
    double* viewPortDataSeriesC = new double[noOfPoints];
    int arraySizeInBytes = noOfPoints * sizeof(double);
    memcpy(viewPortTimeStamps, m_timeStamps.data + startIndex, arraySizeInBytes);
    memcpy(viewPortDataSeriesA, m_dataSeriesA.data + startIndex, arraySizeInBytes);
    memcpy(viewPortDataSeriesB, m_dataSeriesB.data + startIndex, arraySizeInBytes);
    memcpy(viewPortDataSeriesC, m_dataSeriesC.data + startIndex, arraySizeInBytes);

    if (noOfPoints >= 520)
    {
        //
        // Zoomable chart with high zooming ratios often need to plot many thousands of 
        // points when fully zoomed out. However, it is usually not needed to plot more
        // data points than the resolution of the chart. Plotting too many points may cause
        // the points and the lines to overlap. So rather than increasing resolution, this 
        // reduces the clarity of the chart. So it is better to aggregate the data first if
        // there are too many points.
        //
        // In our current example, the chart only has 520 pixels in width and is using a 2
        // pixel line width. So if there are more than 520 data points, we aggregate the 
        // data using the ChartDirector aggregation utility method.
        //
        // If in your real application, you do not have too many data points, you may 
        // remove the following code altogether.
        //

        // Set up an aggregator to aggregate the data based on regular sized slots
        ArrayMath m(DoubleArray(viewPortTimeStamps, noOfPoints));
        m.selectRegularSpacing(noOfPoints / 260);
        
        // For the timestamps, take the first timestamp on each slot
        int aggregatedNoOfPoints = m.aggregate(DoubleArray(viewPortTimeStamps, noOfPoints), 
            Chart::AggregateFirst).len;

        // For the data values, aggregate by taking the averages
        m.aggregate(DoubleArray(viewPortDataSeriesA, noOfPoints), Chart::AggregateAvg);
        m.aggregate(DoubleArray(viewPortDataSeriesB, noOfPoints), Chart::AggregateAvg);
        m.aggregate(DoubleArray(viewPortDataSeriesC, noOfPoints), Chart::AggregateAvg);

        noOfPoints = aggregatedNoOfPoints;
    }

    //
    // Now we have obtained the data, we can plot the chart. 
    //

    ///////////////////////////////////////////////////////////////////////////////////////
    // Step 1 - Configure overall chart appearance. 
    ///////////////////////////////////////////////////////////////////////////////////////

    // Create an XYChart object 600 x 300 pixels in size, with pale blue (0xf0f0ff) 
    // background, black (000000) border, 1 pixel raised effect, and with a rounded frame.
    XYChart *c = new XYChart(600, 300, 0xf0f0ff, 0, 1);
    c->setRoundedFrame(m_extBgColor);
    
    // Set the plotarea at (52, 60) and of size 520 x 192 pixels. Use white (ffffff) 
    // background. Enable both horizontal and vertical grids by setting their colors to 
    // grey (cccccc). Set clipping mode to clip the data lines to the plot area.
    c->setPlotArea(52, 60, 520, 192, 0xffffff, -1, -1, 0xcccccc, 0xcccccc);
    c->setClipping();

    // Add a top title to the chart using 15 pts Times New Roman Bold Italic font, with a 
    // light blue (ccccff) background, black (000000) border, and a glass like raised effect.
    c->addTitle("Zooming and Scrolling Demonstration", "timesbi.ttf", 15
        )->setBackground(0xccccff, 0x0, Chart::glassEffect());

    // Add a bottom title to the chart to show the date range of the axis, with a light blue 
    // (ccccff) background.
    char formattedStartDate[32];
    char formattedEndDate[32];
    strcpy(formattedStartDate, c->formatValue(viewPortStartDate, "{value|mmm dd, yyyy}"));
    strcpy(formattedEndDate, c->formatValue(viewPortEndDate, "{value|mmm dd, yyyy}"));
    char buffer[2048];
    sprintf(buffer, "From <*font=arialbi.ttf*>%s<*/font*> to <*font=arialbi.ttf*>%s<*/font*>"
        " (Duration <*font=arialbi.ttf*>%d<*/font*> days)", formattedStartDate, 
        formattedEndDate, (int)((viewPortEndDate - viewPortStartDate) / 86400.0 + 0.5));
    c->addTitle(Chart::Bottom, buffer, "ariali.ttf", 10)->setBackground(0xccccff);

    // Add a legend box at the top of the plot area with 9pts Arial Bold font with flow layout. 
    c->addLegend(50, 33, false, "arialbd.ttf", 9)->setBackground(Chart::Transparent, 
        Chart::Transparent);

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

    // Add a title to the y-axis
    c->yAxis()->setTitle("Price (USD)", "arialbd.ttf", 9);

    ///////////////////////////////////////////////////////////////////////////////////////
    // Step 2 - Add data to chart
    ///////////////////////////////////////////////////////////////////////////////////////

    // 
    // In this example, we represent the data by lines. You may modify the code below if 
    // you want to use other representations (areas, scatter plot, etc).
    //

    // Add a line layer for the lines, using a line width of 2 pixels
    Layer *layer = c->addLineLayer();
    layer->setLineWidth(2);

    // Now we add the 3 data series to a line layer, using the color red (ff0000), green
    // (00cc00) and blue (0000ff)
    layer->setXData(DoubleArray(viewPortTimeStamps, noOfPoints));
    layer->addDataSet(DoubleArray(viewPortDataSeriesA, noOfPoints), 0xff0000, "Product Alpha");
    layer->addDataSet(DoubleArray(viewPortDataSeriesB, noOfPoints), 0x00cc00, "Product Beta");
    layer->addDataSet(DoubleArray(viewPortDataSeriesC, noOfPoints), 0x0000ff, "Product Gamma");

    ///////////////////////////////////////////////////////////////////////////////////////
    // Step 3 - Set up x-axis scale
    ///////////////////////////////////////////////////////////////////////////////////////
    
    // Set x-axis date scale to the view port date range. 
    c->xAxis()->setDateScale(viewPortStartDate, viewPortEndDate);

    //
    // In the current demo, the x-axis range can be from a few years to a few days. We can 
    // let ChartDirector auto-determine the date/time format. However, for more beautiful 
    // formatting, we set up several label formats to be applied at different conditions. 
    //

    // If all ticks are yearly aligned, then we use "yyyy" as the label format.
    c->xAxis()->setFormatCondition("align", 360 * 86400);
    c->xAxis()->setLabelFormat("{value|yyyy}");
    
    // If all ticks are monthly aligned, then 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 daily algined, then 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()->setMultiFormat(Chart::AllPassFilter(), "{value|dd}");

    // For all other cases (sub-daily ticks), use "hh:nn<*br*>mmm dd" for the first label of
    // a day, and "hh:nn" for other labels.
    c->xAxis()->setFormatCondition("else");
    c->xAxis()->setMultiFormat(Chart::StartOfDayFilter(), 
        "<*font=bold*>{value|hh:nn<*br*>mmm dd}", Chart::AllPassFilter(), "{value|hh:nn}");
    
    ///////////////////////////////////////////////////////////////////////////////////////
    // Step 4 - Set up y-axis scale
    ///////////////////////////////////////////////////////////////////////////////////////
    
    if ((viewer->getZoomDirection() == Chart::DirectionHorizontal) || 
        ((m_minValue == 0) && (m_maxValue == 0)))
    {
        // y-axis is auto-scaled - save the chosen y-axis scaled to support xy-zoom mode
        c->layout();
        m_minValue = c->yAxis()->getMinValue();
        m_maxValue = c->yAxis()->getMaxValue();
    }
    else
    {
        // xy-zoom mode - compute the actual axis scale in the view port 
        double axisLowerLimit =  m_maxValue - (m_maxValue - m_minValue) * 
            (viewer->getViewPortTop() + viewer->getViewPortHeight());
        double axisUpperLimit =  m_maxValue - (m_maxValue - m_minValue) * 
            viewer->getViewPortTop();
        // *** use the following formula if you are using a log scale axis ***
        // double axisLowerLimit = m_maxValue * pow(m_minValue / m_maxValue, 
        //  viewer->getViewPortTop() + viewer->getViewPortHeight());
        // double axisUpperLimit = m_maxValue * pow(m_minValue / m_maxValue, 
        //  viewer->getViewPortTop());

        // use the zoomed-in scale
        c->yAxis()->setLinearScale(axisLowerLimit, axisUpperLimit);
        c->yAxis()->setRounding(false, false);
    }

    ///////////////////////////////////////////////////////////////////////////////////////
    // Step 5 - Display the chart
    ///////////////////////////////////////////////////////////////////////////////////////

    // Set the chart image to the WinChartViewer
    delete m_ChartViewer.getChart();
    m_ChartViewer.setChart(c);

    // Free resources
    delete[] viewPortTimeStamps;
    delete[] viewPortDataSeriesA;
    delete[] viewPortDataSeriesB;
    delete[] viewPortDataSeriesC;
}

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

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

//
// Get the default background color
//
int CZoomscrolldemoDlg::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 CZoomscrolldemoDlg::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));  
}