|
Scrolling realtime data |
Posted by MikeP on Jun-04-2013 01:01 |
|
Hello.
I'm trying to use ChartDirector to display a line chart of realtime data which allows
horizontal scrolling. Since I do not know the amount of data that will ultimately come in, I
use an std::vector to store my data. The internal array is then passed to DoubleArray for
display. I've used the simplezoomscroll Qt example as a starting point and I think I have it
working with a few issues I was hoping to get resolved here.
1) How can I 'fix' the x-axis and grid lines to the data? In the current version the x-axis
labels and vertical grid lines pop back and forth probably due to sync'ing and/or auto-
scaling. If I could set the x-axis labels as an enumeration I think it would work the way
I'd like, but it appears enumeration and messing with the viewport don't interact well
together.
2) Am I properly updating the viewport when new data arrives (see addData() method)?
My understanding is that the viewport tracks ratios/percentages (eg, viewPortLeft,
viewPortWidth), so when new data arrives I need to readjust my ratios to maintain proper
viewport positioning (eg, left) and dimensions (eg, width). However, by dragging slowly
(click and drag to the right) the line on the chart actually moves in the wrong direction
(to the left, against the mouse direction).
I've included my code below. Any help would be extremely appreciated.
Thanks.
<SimpleZoomScroll.cpp>-------------------------------------------------------
#include <QtGui>
#include <QApplication>
#include <QPushButton>
#include <QButtonGroup>
#include <math.h>
#include "simplezoomscroll.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setStyleSheet("* {font-family:arial;font-size:11px}");
SimpleZoomScroll demo;
demo.show();
return app.exec();
}
SimpleZoomScroll::SimpleZoomScroll(QWidget *parent) :
QDialog(parent)
{
//
// Set up the GUI
//
setFixedSize(732, 308);
setWindowTitle("Simple Zooming and Scrolling");
// The frame on the left side
QFrame *frame = new QFrame(this);
frame->setGeometry(4, 4, 120, 300);
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)));
// Chart Viewer
m_ChartViewer = new QChartViewer(this);
m_ChartViewer->setGeometry(128, 4, 650, 350);
connect(m_ChartViewer, SIGNAL(viewPortChanged()), SLOT(onViewPortChanged()));
connect(m_ChartViewer, SIGNAL(mouseMovePlotArea(QMouseEvent*)),
SLOT(onMouseMovePlotArea(QMouseEvent*)));
connect(m_ChartViewer, SIGNAL(mouseWheel(QWheelEvent*)),
SLOT(onMouseWheelChart(QWheelEvent*)));
//
// Initialize the chart
//
// Load the data
loadData();
QTimer* addDataTimer = new QTimer(this);
connect(addDataTimer, SIGNAL(timeout()), this, SLOT(addData()));
addDataTimer->start(1000);
// Initialize the QChartViewer
initChartViewer(m_ChartViewer);
// Initially set the mouse to drag to scroll mode
pointerPB->click();
// Trigger the ViewPortChanged event to draw the chart
m_ChartViewer->updateViewPort(true, true);
}
SimpleZoomScroll::~SimpleZoomScroll()
{
delete m_ranSeries;
delete m_ChartViewer->getChart();
}
//
// Load the data
//
void SimpleZoomScroll::loadData()
{
// In this example, we just use random numbers as data.
m_ranSeries = new RanSeries(127);
DoubleArray ts = m_ranSeries->getDateSeries(100, Chart::chartTime(2007, 1, 1),
86400);
DoubleArray dsa = m_ranSeries->getSeries(100, 150, -10, 10);
DoubleArray dsb = m_ranSeries->getSeries(100, 200, -10, 10);
DoubleArray dsc = m_ranSeries->getSeries(100, 250, -8, 8);
m_ts = std::vector<double>(ts.data, ts.data + ts.len);
m_dsa = std::vector<double>(dsa.data, dsa.data + dsa.len);
m_dsb = std::vector<double>(dsb.data, dsb.data + dsb.len);
m_dsc = std::vector<double>(dsc.data, dsc.data + dsc.len);
m_timeStamps = DoubleArray(&m_ts[0], int(m_ts.size()));
m_dataSeriesA = DoubleArray(&m_dsa[0], int(m_dsa.size()));
m_dataSeriesB = DoubleArray(&m_dsb[0], int(m_dsb.size()));
m_dataSeriesC = DoubleArray(&m_dsc[0], int(m_dsc.size()));
}
void SimpleZoomScroll::addData()
{
int oldSize = int(m_ts.size());
double oldVPWidth = m_ChartViewer->getViewPortWidth();
double oldVPLeft = m_ChartViewer->getViewPortLeft();
m_ts.push_back(m_ts[m_ts.size()-1]+86400);
m_dsa.push_back(m_ranSeries->getSeries(10, 150, -10, 10).data[5]);
m_dsb.push_back(m_ranSeries->getSeries(10, 200, -10, 10).data[5]);
m_dsc.push_back(m_ranSeries->getSeries(10, 250, -8, 8).data[5]);
m_timeStamps = DoubleArray(&m_ts[0], int(m_ts.size()));
m_dataSeriesA = DoubleArray(&m_dsa[0], int(m_dsa.size()));
m_dataSeriesB = DoubleArray(&m_dsb[0], int(m_dsb.size()));
m_dataSeriesC = DoubleArray(&m_dsc[0], int(m_dsc.size()));
int newSize = int(m_ts.size());
m_ChartViewer->setFullRange("x", m_timeStamps[0],
m_timeStamps[m_timeStamps.len - 1]);
m_ChartViewer->setViewPortWidth(oldVPWidth * oldSize / newSize);
m_ChartViewer->setViewPortLeft(oldVPLeft * oldSize / newSize);
}
//
// Initialize the QChartViewer
//
void SimpleZoomScroll::initChartViewer(QChartViewer *viewer)
{
// Set the full x range to be the duration of the data
viewer->setFullRange("x", m_timeStamps[0], m_timeStamps[m_timeStamps.len - 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.len);
}
//
// 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 WinChartViewer.updateViewPort.
//
void SimpleZoomScroll::onViewPortChanged()
{
if (m_ChartViewer->needUpdateChart())
drawChart(m_ChartViewer);
if (m_ChartViewer->needUpdateImageMap())
updateImageMap(m_ChartViewer);
}
//
// Draw the chart and display it in the given viewer
//
void SimpleZoomScroll::drawChart(QChartViewer *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->getViewPortLeft()
+
viewer->getViewPortWidth());
// Get the array indexes that corresponds to the visible start and end dates
int startIndex = (int)floor(Chart::bSearch(m_timeStamps, viewPortStartDate));
int endIndex = (int)ceil(Chart::bSearch(m_timeStamps, viewPortEndDate));
int noOfPoints = endIndex - startIndex + 1;
// Extract the part of the data array that are visible.
DoubleArray viewPortTimeStamps = DoubleArray(m_timeStamps.data + startIndex,
noOfPoints);
DoubleArray viewPortDataSeriesA = DoubleArray(m_dataSeriesA.data + startIndex,
noOfPoints);
DoubleArray viewPortDataSeriesB = DoubleArray(m_dataSeriesB.data + startIndex,
noOfPoints);
DoubleArray viewPortDataSeriesC = DoubleArray(m_dataSeriesC.data + startIndex,
noOfPoints);
//
// At this stage, we have extracted the visible data. We can use those data to plot
the chart.
//
///////////////////////////////////////////////////////////////////////////////////////
// Configure overall chart appearance.
///////////////////////////////////////////////////////////////////////////////////////
// Create an XYChart object 600 x 300 pixels in size, with pale blue (0xf0f0ff)
background,
// black (000000) rounded border, 1 pixel raised effect.
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 205 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, 205, 0xffffff, -1, -1, 0xcccccc, 0xcccccc);
// As the data can lie outside the plotarea in a zoomed chart, we need to enable
clipping.
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("Simple Zooming and Scrolling", "timesbi.ttf", 15
)->setBackground(0xccccff, 0x0, Chart::glassEffect());
// 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);
///////////////////////////////////////////////////////////////////////////////////////
// Add data to chart
///////////////////////////////////////////////////////////////////////////////////////
//
// In this example, we represent the data by lines. You may modify the code below to
use other
// representations (areas, scatter plot, etc).
//
// Add a line layer for the lines, using a line width of 2 pixels
LineLayer *layer = c->addLineLayer();
layer->setLineWidth(2);
// In this demo, we do not have too many data points. In real code, the chart may
contain a lot
// of data points when fully zoomed out - much more than the number of horizontal
pixels in this
// plot area. So it is a good idea to use fast line mode.
layer->setFastLineMode();
// Now we add the 3 data series to a line layer, using the color red (ff0000), green
// (00cc00) and blue (0000ff)
layer->setXData(viewPortTimeStamps);
layer->addDataSet(viewPortDataSeriesA, 0xff0000, "Product Alpha");
layer->addDataSet(viewPortDataSeriesB, 0x00cc00, "Product Beta");
layer->addDataSet(viewPortDataSeriesC, 0x0000ff, "Product Gamma");
///////////////////////////////////////////////////////////////////////////////////////
// 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->syncDateAxisWithViewPort("x", c->xAxis());
//c->xAxis()->setDateScale();
//c->xAxis()->setLabels(m_timeStamps);
// In this demo, we rely on ChartDirector to auto-label the axis. We ask ChartDirector
to ensure
// the x-axis labels are at least 75 pixels apart to avoid too many labels.
c->xAxis()->setTickDensity(75);
///////////////////////////////////////////////////////////////////////////////////////
// Output the chart
///////////////////////////////////////////////////////////////////////////////////////
delete viewer->getChart();
viewer->setChart(c);
}
//
// Update the image map
//
void SimpleZoomScroll::updateImageMap(QChartViewer *viewer)
{
// Include tool tip for the chart
if (0 == viewer->getImageMapHandler())
{
viewer->setImageMap(viewer->getChart()->getHTMLImageMap("", "",
"title='[{dataSetName}] {x|mmm dd, yyyy}: USD {value|2}'"));
}
}
//
// The Pointer, Zoom In or Zoom out button is pressed
//
void SimpleZoomScroll::onMouseUsageChanged(int mouseUsage)
{
m_ChartViewer->setMouseUsage(mouseUsage);
}
<SimpleZoomScroll.h>-------------------------------------------------------
#ifndef SIMPLEZOOMSCROLL_H
#define SIMPLEZOOMSCROLL_H
#include <vector>
#include <QDialog>
#include "qchartviewer.h"
class SimpleZoomScroll : public QDialog {
Q_OBJECT
public:
SimpleZoomScroll(QWidget *parent = 0);
~SimpleZoomScroll();
private:
std::vector<double> m_ts;
std::vector<double> m_dsa;
std::vector<double> m_dsb;
std::vector<double> m_dsc;
//
// Data arrays for the scrollable / zoomable chart.
// - In this demo, we just use a RanTable object to generate random data for the
chart.
//
RanSeries *m_ranSeries;
DoubleArray m_timeStamps;
DoubleArray m_dataSeriesA;
DoubleArray m_dataSeriesB;
DoubleArray m_dataSeriesC;
QChartViewer *m_ChartViewer;
void loadData(); // Load data into data arrays
void initChartViewer(QChartViewer *viewer); // Initialize the QChartViewer
void drawChart(QChartViewer *viewer); // Draw chart
void updateImageMap(QChartViewer *viewer); // Update the image map
private slots:
void addData();
void onMouseUsageChanged(int mouseUsage);
void onViewPortChanged();
};
#endif // SIMPLEZOOMSCROLL_H |
Re: Scrolling realtime data |
Posted by MikeP on Jun-04-2013 04:08 |
|
I'm still looking through old posts, but I think I may have been able to remedy #2 based
on the response given here:
http://www.chartdir.com/forum/download_thread.php?
bn=chartdir_support&pattern=scrolling&thread=1350907806#N1351063742
Sounds like manually updating the viewport and scrolling don't work well together since
scrolling will 'snapshot' the viewport, thus putting the manual and scrolling viewport
values in conflict with each other. I added an isDragScrolling() method to the
QChartViewer class and use that value to determine whether or not to update the
DoubleArrays and viewport values (left and width). The new addData method now looks
like this:
void SimpleZoomScroll::addData()
{
m_ts.push_back(m_ts[m_ts.size()-1]+86400);
m_dsa.push_back(m_ranSeries->getSeries(10, 150, -10, 10).data[5]);
m_dsb.push_back(m_ranSeries->getSeries(10, 200, -10, 10).data[5]);
m_dsc.push_back(m_ranSeries->getSeries(10, 250, -8, 8).data[5]);
if( !m_ChartViewer->isDragScrolling() )
{
int oldSize = m_timeStamps.len;
m_timeStamps = DoubleArray(&m_ts[0], int(m_ts.size()));
m_dataSeriesA = DoubleArray(&m_dsa[0], int(m_dsa.size()));
m_dataSeriesB = DoubleArray(&m_dsb[0], int(m_dsb.size()));
m_dataSeriesC = DoubleArray(&m_dsc[0], int(m_dsc.size()));
int newSize = m_timeStamps.len;
m_ChartViewer->setFullRange("x", m_timeStamps[0],
m_timeStamps[m_timeStamps.len - 1]);
m_ChartViewer->setViewPortWidth(m_ChartViewer->getViewPortWidth() *
oldSize / newSize);
m_ChartViewer->setViewPortLeft(m_ChartViewer->getViewPortLeft() *
oldSize / newSize);
}
}
With this method I no longer see the odd behavior I was seeing before. However, it might
make tracking latest data a little cumbersome (ie, updating the chart to show the latest
value when scrolled all the way to the right). I would still be interested to know if what
I'm doing is a 'correct' way of doing it.
As for the x-axis labels and grid lines, I could really use some help on that. I've tried
setting labels explicitly, tried using setDateScale, tried using format conditions with multi-
formats (as seen in the zoomscrolltrack demo), etc etc. Basically I would like the labels /
grid lines to not 'pop' to a new value constantly, thereby getting rid of the constant
judder that occurs while scrolling. There has to be a way to 'fix' the, say, major ticks
such that they don't keep popping on and off while using syncDateWithViewPort, no? |
Re: Scrolling realtime data |
Posted by Peter Kwan on Jun-04-2013 18:55 |
|
Hi MikeP,
For the drag scrolling behaviour, you can modify QChartViewer.cpp so that it retakes a snapshot everytime a drag to scroll occurs (instead of taking the snapshot at the start of the drag). In the original QChartViewer.cpp, there are the lines:
if (dragTo(m_scrollDirection,
event->x() - m_plotAreaMouseDownXPos, event->y() - m_plotAreaMouseDownYPos))
updateViewPort(true, false);
Please change the above to:
startDrag();
if (dragTo(m_scrollDirection,
event->x() - m_plotAreaMouseDownXPos, event->y() - m_plotAreaMouseDownYPos))
updateViewPort(true, false);
m_plotAreaMouseDownXPos = event->x();
m_plotAreaMouseDownYPos = event->y();
This should allow you to change the view port while dragging to scroll.
For the x-axis grid lines, the behaviour you are seeing occurs for some types of date/time labels because there is no natural "alignment" for those labels. For "multi-day" labels, ChartDirector simply labels the axis starting from the first day on the axis. This is the normal behaviour of ChartDirector. Other ChartDirector sample programs also have this behaviour.
For some labels (eg. numeric or hourly labels), there are natural alignments. For example, for numeric labels, if ChartDirector decides that the spacing between the labels should be 5 units, the labels always end with 5 or 0, like 20, 25, 30, 35, ... ChartDirector will not choose other odd alignments like 23, 28, 33, ...
On the other hand, for a date/time axis, if the labels should be 5 days apart, there is no natural alignment. The day of month can be 1, 6, 11, 16, ... or it can be 2, 7, 12, 17 ... Both are possible (we cannot force the labels to be at the first day of every month and still maintain a regular 5 days increment). So ChartDirector simply uses the first day on the axis to start the labelling. As the first date scrolls from 1 to 2, the labels change from 1, 6, 11, 16 to 2, 7, 12, 17, so the labels can change when the chart scrolls.
If you would like similar behaviour like the numeric axis, you may "re-label" the axis. The following is an example (put the code just before "delete viewer->getChart();":
//auto-scale axis to obtain the automatically determined tick increment
c->layoutAxes();
DoubleArray ticks = c->xAxis()->getTicks();
double increment = ticks[1] - ticks[0];
//cancel the automatic labels
for (int i = 0; i < ticks.len; ++i)
c->xAxis()->addLabel(ticks[i], 0);
//re-label the axis using labels that aligns to Jan 1, 0001 00:00:00 (it means if the axis is extends indefinitely,
//the tick will include Jan 1, 0001 00:00:00).
for (double tickStart = ceil(c->xAxis()->getMinValue() / increment) * increment;
tickStart <= c->xAxis()->getMaxValue(); tickStart += increment)
c->xAxis()->addLabel(tickStart, c->formatValue(tickStart, "{value|mm/dd/yyyy}"));
Hope this can help.
Regards
Peter Kwan |
Re: Scrolling realtime data |
Posted by MikeP on Jun-14-2013 23:26 |
|
Hi Peter.
Sorry for the delayed response. Both suggestions worked perfectly. Thanks for the
assistance.
MikeP |
|