ChartDirector Ver 5.0 (C++ Edition)

Zooming and Scrolling Demonstration (QT)




NOTE: This section describes Zooming and Scrolling Demonstration for QT only. For MFC, please refer to Zooming and Scrolling Demonstration (MFC).

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

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 QT project is in "qtdemo/zoomscrolldemo".

[File: qtdemo/zoomscrolldemo/zoomscrolldemo.cpp]
#include <QApplication>
#include <QPushButton>
#include <QLineEdit>
#include <QButtonGroup>
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include "zoomscrolldemo.h"
#include "hotspotdialog.h"


int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    ZoomScrollDemo demo;
    demo.show();
    return app.exec();
}


//
// Because QT uses QDateTime, while ChartDirector uses Chart::chartTime, we need
// utilities to convert from one to another
//

// Convert from QDateTime to chartTime
static double QDateTimeToChartTime(QDateTime q)
{
    QDate d = q.date();
    QTime t = q.time();
    return Chart::chartTime(d.year(), d.month(), d.day(), t.hour(), t.minute(),
                            t.second()) + t.msec() / 1000.0;
}

// Convert from chartTime to QDateTime
static QDateTime ChartTimeToQDateTime(double t)
{
    double ymdhms = floor(t);
    int ms = (int)(floor((t - ymdhms) * 1000));
    int ymd = Chart::getChartYMD(ymdhms);
    int hms = (int)fmod(ymdhms, 86400);

    return QDateTime(QDate(ymd / 10000, (ymd % 10000) / 100, ymd % 100),
        QTime(hms / 3600, (hms % 3600) / 60, hms % 60, ms));
}

ZoomScrollDemo::ZoomScrollDemo(QWidget *parent) :
    QDialog(parent)
{
    //
    // Set up the GUI
    //

    setFont(QFont("Arial", 9));
    setFixedSize(758, 334);
    setWindowTitle("Zooming and Scrolling Demonstration");

    // The frame on the left side
    QFrame *frame = new QFrame(this);
    frame->setGeometry(4, 4, 120, 326);
    frame->setFrameShape(QFrame::StyledPanel);

    // Pointer push button
    QPushButton *pointerPB = new QPushButton(QIcon(":/pointer.png"), "Pointer", frame);
    pointerPB->setGeometry(4, 8, 112, 28);
    pointerPB->setStyleSheet("QPushButton { text-align:left; padding:5px}");
    pointerPB->setCheckable(true);

    // Zoom In push button
    QPushButton *zoomInPB = new QPushButton(QIcon(":/zoomin.png"), "Zoom In", frame);
    zoomInPB->setGeometry(4, 36, 112, 28);
    zoomInPB->setStyleSheet("QPushButton { text-align:left; padding:5px}");
    zoomInPB->setCheckable(true);

    // Zoom Out push button
    QPushButton *zoomOutPB = new QPushButton(QIcon(":/zoomout.png"), "Zoom Out", frame);
    zoomOutPB->setStyleSheet("QPushButton { text-align:left; padding:5px}");
    zoomOutPB->setGeometry(4, 64, 112, 28);
    zoomOutPB->setCheckable(true);

    // The Pointer/Zoom In/Zoom Out buttons form a button group
    QButtonGroup *mouseUsage = new QButtonGroup(frame);
    mouseUsage->addButton(pointerPB, Chart::MouseUsageScroll);
    mouseUsage->addButton(zoomInPB, Chart::MouseUsageZoomIn);
    mouseUsage->addButton(zoomOutPB, Chart::MouseUsageZoomOut);
    connect(mouseUsage, SIGNAL(buttonPressed(int)), SLOT(onMouseUsageChanged(int)));

    // Zoom Mode label
    (new QLabel("Zoom Mode", frame))->setGeometry(6, 104, 112, 18);
    
    // X Zoom push button
    QPushButton *xZoomPB = new QPushButton(QIcon(":/xrange.png"), "X Zoom", frame);
    xZoomPB->setGeometry(4, 122, 112, 28);
    xZoomPB->setStyleSheet("QPushButton { text-align:left; padding:5px}");
    xZoomPB->setCheckable(true);

    // XY Zoom push button
    QPushButton *xyZoomPB = new QPushButton(QIcon(":/xyrange.png"), "XY Zoom", frame);
    xyZoomPB->setGeometry(4, 150, 112, 28);
    xyZoomPB->setStyleSheet("QPushButton { text-align:left; padding:5px}");
    xyZoomPB->setCheckable(true);

    // The X Zoom/XY Zoom buttons form a button group
    QButtonGroup *zoomMode = new QButtonGroup(frame);
    zoomMode->addButton(xZoomPB, Chart::DirectionHorizontal);
    zoomMode->addButton(xyZoomPB, Chart::DirectionHorizontalVertical);
    connect(zoomMode, SIGNAL(buttonPressed(int)), SLOT(onZoomModeChanged(int)));

    // Start Date control
    (new QLabel("Start Date", frame))->setGeometry(6, 190, 112, 18);
    m_StartDate = new QDateEdit(frame);
    m_StartDate->setGeometry(4, 208, 113, 22);
    connect(m_StartDate, SIGNAL(dateTimeChanged(QDateTime)), SLOT(onStartDateChanged(QDateTime)));

    // Duration control
    (new QLabel("Duration (Days)", frame))->setGeometry(6, 244, 112, 18);
    m_Duration = new QComboBox(frame);
    m_Duration->setGeometry(4, 262, 113, 22);
    m_Duration->setEditable(true);
    m_Duration->setInsertPolicy(QComboBox::NoInsert);
    m_Duration->addItems(QStringList() << "10" << "20" << "30" << "60" << "90" << "180"
                             << "360" << "720" << "1080" << "1440");
    connect(m_Duration, SIGNAL(editTextChanged(QString)), SLOT(onDurationChanged(QString)));

    // Chart Viewer
    m_ChartViewer = new QChartViewer(this);
    m_ChartViewer->setGeometry(132, 8, 600, 300);
    connect(m_ChartViewer, SIGNAL(viewPortChanged()), SLOT(onViewPortChanged()));
    connect(m_ChartViewer, SIGNAL(mouseMove(QMouseEvent*)), SLOT(onMouseMove(QMouseEvent*)));
    connect(m_ChartViewer, SIGNAL(clicked(QMouseEvent*)), SLOT(onChartClicked(QMouseEvent*)));

    // Horizontal scroll bar
    m_HScrollBar = new QScrollBar(Qt::Horizontal, this);
    m_HScrollBar->setGeometry(132, 316, 608, 17);
    connect(m_HScrollBar, SIGNAL(valueChanged(int)), SLOT(onHScrollBarChanged(int)));

    // Vertical scroll bar
    m_VScrollBar = new QScrollBar(Qt::Vertical, this);
    m_VScrollBar->setGeometry(740, 0, 17, 316);
    connect(m_VScrollBar, SIGNAL(valueChanged(int)), SLOT(onVScrollBarChanged(int)));

    //
    // Initialize member variables
    //

    // y axes ranges
    m_minValue = m_maxValue = 0;

    // 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;

    // 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.
    pointerPB->click();
    xZoomPB->click();
    m_ChartViewer->updateViewPort(true, true);
}

ZoomScrollDemo::~ZoomScrollDemo()
{
    delete m_dataTable;
    delete m_ChartViewer->getChart();
}

//
// The Pointer, Zoom In or Zoom out button is pressed
//
void ZoomScrollDemo::onMouseUsageChanged(int mouseUsage)
{
    m_ChartViewer->setMouseUsage(mouseUsage);
}

//
// The X Zoom or XY Zoom button is pressed
//
void ZoomScrollDemo::onZoomModeChanged(int zoomMode)
{
    m_ChartViewer->setZoomDirection(zoomMode);
    m_ChartViewer->setScrollDirection(zoomMode);

    if (zoomMode == 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 selects a start date from the QDateEdit control
//
void ZoomScrollDemo::onStartDateChanged(QDateTime date)
{
    // Compute the new view port position based on the selected date
    m_ChartViewer->setViewPortLeft(
        (QDateTimeToChartTime(date) - m_minDate) / m_dateRange);
    m_ChartViewer->updateViewPort(true, true);
}

//
// User selects a duration from the Duration combo box
//
void ZoomScrollDemo::onDurationChanged(QString s)
{
    // Parse the duration text
    double newDuration = s.toDouble() * 86400;
    if (newDuration == 0)
        return;

    // 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);
    }
}

//
// User clicks on the the horizontal scroll bar
//
void ZoomScrollDemo::onHScrollBarChanged(int value)
{
    // Set the view port based on the scroll bar
    int scrollBarLen = m_HScrollBar->maximum() + m_HScrollBar->pageStep();
    m_ChartViewer->setViewPortLeft(value / (double)scrollBarLen);

    // Update the chart display without updating the image maps. (We can delay updating
    // the image map until scrolling is completed and the chart display is stable.)
    m_ChartViewer->updateViewPort(true, false);
}

//
// User clicks on the the vertical scroll bar
//
void ZoomScrollDemo::onVScrollBarChanged(int value)
{
    // Set the view port based on the scroll bar
    int scrollBarLen = m_VScrollBar->maximum() + m_VScrollBar->pageStep();
    m_ChartViewer->setViewPortTop(value / (double)scrollBarLen);

    // Update the chart display without updating the image maps. (We can delay updating
    // the image map until scrolling is completed and the chart display is stable.)
    m_ChartViewer->updateViewPort(true, false);
}

//
// QChartViewer ViewPortChanged event
//
void ZoomScrollDemo::onViewPortChanged()
{
    QChartViewer *viewer = (QChartViewer *)QObject::sender();

    // Update other controls in the user interface
    updateControls(viewer);

    //
    // Update chart and image map if necessary
    //
    if (viewer->needUpdateChart())
        drawChart(viewer);
    if (viewer->needUpdateImageMap())
        updateImageMap(viewer);
}

//
// Update controls in the user interface when the view port changed
//
void ZoomScrollDemo::updateControls(QChartViewer *viewer)
{
    //
    // Update the scroll bars to reflect the view port setting
    //

    // The logical length of the scrollbar. It can be any large value. The actual value does
    // not matter.
    const int scrollBarLen = 1000000000;

    // Update the horizontal scroll bar
    m_HScrollBar->setEnabled(viewer->getViewPortWidth() < 1);
    m_HScrollBar->setPageStep((int)ceil(viewer->getViewPortWidth() * scrollBarLen));
    m_HScrollBar->setRange(0, scrollBarLen - m_HScrollBar->pageStep());
    m_HScrollBar->setValue((int)(0.5 + viewer->getViewPortLeft() * scrollBarLen));

    // Update the vertical scroll bar
    m_VScrollBar->setEnabled(viewer->getViewPortHeight() < 1);
    m_VScrollBar->setPageStep((int)ceil(viewer->getViewPortHeight() * scrollBarLen));
    m_VScrollBar->setRange(0, scrollBarLen - m_VScrollBar->pageStep());
    m_VScrollBar->setValue((int)(0.5 + viewer->getViewPortTop() * scrollBarLen));

    //
    // Update the start date in the QDataEdit control.
    //
    double currentStartDate = m_minDate + (int)(0.5 + viewer->getViewPortLeft()
        * m_dateRange);
    m_StartDate->setDateTimeRange(ChartTimeToQDateTime(m_minDate),
                                  ChartTimeToQDateTime(m_minDate + m_dateRange));
    m_StartDate->setDateTime(ChartTimeToQDateTime(currentStartDate));

    //
    // Update the duration in the QComboBox
    //
    m_currentDuration = (int)(0.5 + viewer->getViewPortWidth() * m_dateRange);
    m_Duration->lineEdit()->setText(QString("%1").arg((int)(0.5 + m_currentDuration / 86400)));
}

//
// User clicks on the QChartViewer
//
void ZoomScrollDemo::onChartClicked(QMouseEvent *)
{
    QChartViewer *viewer = (QChartViewer *)QObject::sender();
    ImageMapHandler *handler = viewer->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.
            HotSpotDialog hs;
            hs.setData(handler);
            hs.exec();
        }
    }
}

//
// Load the data
//
void ZoomScrollDemo::loadData()
{
    // In this demo, we allow scrolling for the last 5 years.
    QDateTime t = QDateTime::currentDateTime();
    double lastDate = QDateTimeToChartTime(t);
    double firstDate = QDateTimeToChartTime(t.addYears(-5));

    // The initial view port is to show 1 year of data.
    m_currentDuration = lastDate - QDateTimeToChartTime(t.addYears(-1));

    //
    // 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);
}

//
// Draw the chart and display it in the given viewer
//
void ZoomScrollDemo::drawChart(QChartViewer *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 + floor(viewer->getViewPortLeft() *
        m_dateRange + 0.5);
    double viewPortEndDate = viewPortStartDate + floor(viewer->getViewPortWidth() *
        m_dateRange + 0.5);

    // Get the starting index of the array using the start date
    int startIndex = (int)(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 = (int)(std::upper_bound(m_timeStamps.data, m_timeStamps.data + m_timeStamps.len,
        viewPortEndDate) - m_timeStamps.data);
        if (endIndex >= m_timeStamps.len - 1)
                endIndex = m_timeStamps.len - 1;

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

    // 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);
    QColor bgColor = palette().color(backgroundRole()).rgb();
    c->setRoundedFrame((bgColor.red() << 16) + (bgColor.green() << 8) + bgColor.blue());

    // 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 viewer->getChart();
    viewer->setChart(c);

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

//
// Update the image map
//
void ZoomScrollDemo::updateImageMap(QChartViewer *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}'"));
    }
}

//
// When the mouse enters the chart, we will generate an image map for hot spots and tooltips
// support if it has not already been generated.
//
void ZoomScrollDemo::onMouseMove(QMouseEvent *)
{
    updateImageMap((QChartViewer *)QObject::sender());
}