PDA

View Full Version : How to plot a simple function with QDateTime X axis and int/double Y axis?



falconium
28th April 2011, 21:25
Hi,

It looks like all the included examples are to difficult to me (moving knobs, time-based changes in charts, etc.).
I simply cannot figure out the basic use of the library.
Can someone please include here a very-very simple example of a class that can be put in a layout as widget and shown that has only X-Y axes as described in the topic. Can someone include a simple example of Y=2X (where X is QDateTime that is converted to double for example).

My source data is in QStandardItemModel. First column is QDateTime (real dates and times of logged events), all the other columns are measured values of different objects - so basically different lines in the chart.


Thank you in advance!

I include the source where I got, but it is just unreliable. :(

HEADER:

#ifndef CHARTPLOT_H
#define CHARTPLOT_H

#include <qwt_plot.h>
#include <QStandardItemModel>

class ChartCurve;

class ChartPlot : public QwtPlot
{
Q_OBJECT
public:
ChartPlot(QStandardItemModel *model, QWidget * = 0);
const ChartCurve *getCurve() const
{
return curve;
}

void setModel(QStandardItemModel *model)
{
this->model = model;
}

QStandardItemModel *getModel()
{
return this->model;
}

private Q_SLOTS:
void showCurve(QwtPlotItem *, bool on);

private:
QStandardItemModel *model;
ChartCurve *curve;
};

#endif


SOURCE:

#include <qapplication.h>
#include <qlayout.h>
#include <qlabel.h>
#include <qtime>
#include <qpainter.h>
#include <qwt_plot_layout.h>
#include <qwt_plot_curve.h>
#include <qwt_scale_draw.h>
#include <qwt_scale_widget.h>
#include <qwt_legend.h>
#include <qwt_legend_item.h>
#include "chartplot.h"
#include <QDebug>

class TimeScaleDraw: public QwtScaleDraw
{
public:
TimeScaleDraw(const QTime &base) : baseTime(base)
{
}

virtual QwtText label(double v) const
{
QTime upTime = baseTime.addSecs((int)v);
return upTime.toString();
}
private:
QTime baseTime;
};

class Background: public QwtPlotItem
{
public:
Background()
{
setZ(0.0);
}

virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}

virtual void draw(QPainter *painter,
const QwtScaleMap &, const QwtScaleMap &yMap,
const QRectF &rect) const
{
QColor c(Qt::gray);
c=c.darker(150);
QRectF r = rect;

r.setBottom(yMap.transform(500));
r.setTop(yMap.transform(0));
painter->fillRect(r, c);
}
};

class ChartCurve: public QwtPlotCurve
{
public:
ChartCurve(const QString &title) : QwtPlotCurve(title)
{
setRenderHint(QwtPlotItem::RenderAntialiased);
}

void setColor(const QColor &color)
{
QColor c = color;
c.setAlpha(150);

setPen(c);
setBrush(c);
}
};

ChartPlot::ChartPlot(QStandardItemModel *model, QWidget *parent) : QwtPlot(parent)
{
setAutoReplot(false);

plotLayout()->setAlignCanvasToScales(true);

QwtLegend *legend = new QwtLegend;
legend->setItemMode(QwtLegend::CheckableItem);
insertLegend(legend, QwtPlot::RightLegend);

setAxisTitle(QwtPlot::xBottom, "Report time");
//setAxisScaleDraw(QwtPlot::xBottom, new TimeScaleDraw(cpuStat.upTime()));
setAxisScale(QwtPlot::xBottom, 0, 10);
setAxisLabelRotation(QwtPlot::xBottom, -45.0);
setAxisLabelAlignment(QwtPlot::xBottom, Qt::AlignLeft | Qt::AlignBottom);

/*
In situations, when there is a label at the most right position of the
scale, additional space is needed to display the overlapping part
of the label would be taken by reducing the width of scale and canvas.
To avoid this "jumping canvas" effect, we add a permanent margin.
We don't need to do the same for the left border, because there
is enough space for the overlapping label below the left scale.
*/

QwtScaleWidget *scaleWidget = axisWidget(QwtPlot::xBottom);
const int fmh = QFontMetrics(scaleWidget->font()).height();
scaleWidget->setMinBorderDist(0, fmh / 2);

setAxisTitle(QwtPlot::yLeft, "Quantity");
setAxisScale(QwtPlot::yLeft, 0, 100);

Background *bg = new Background();
bg->attach(this);

curve = new ChartCurve("");
curve->setColor(Qt::blue);
curve->setZ(curve->z() - 1);
curve->attach(this);

double xData[model->rowCount()];
double yData[model->rowCount()];
qDebug() <<"ROWCOUNT:"<<model->rowCount();
for (int x=0; x<model->rowCount(); x++)
{
//xData[x] = model->item(x, 0)->data(Qt::EditRole).toDateTime();
xData[x] = x+1;
qDebug()<<"X:"<<xData[x];
}
for (int y=0; y<model->rowCount(); y++)
{
yData[y] = model->item(y, 1)->data(Qt::EditRole).toFloat();
qDebug()<<"Y:"<<yData[y];
}

curve->setRawSamples(xData, yData, model->rowCount());
showCurve(curve, true);

//for (int i = 0; i < 100; i++) xData[100 - 1 - i] = i;

connect(this, SIGNAL(legendChecked(QwtPlotItem *, bool)), SLOT(showCurve(QwtPlotItem *, bool)));
}

void ChartPlot::showCurve(QwtPlotItem *item, bool on)
{
item->setVisible(on);
QWidget *w = legend()->find(item);
if (w && w->inherits("QwtLegendItem")) ((QwtLegendItem *)w)->setChecked(on);

replot();
}


I have tried to modify CPU plot to simple x/y function. No success.

Thanks!

corrado1972
29th April 2011, 10:08
I faced your problem too: if you want to learn how to use an x datetime axis, (that is not changing its values in the time like cpuplot), see friedberg (https://qwt.svn.sourceforge.net/svnroot/qwt/branches/qwt-6.0/examples/friedberg/) example.
(I'm using qwt 6.0)

Uwe
29th April 2011, 12:42
Scales are always based on doubles, but you can always translate date/time values into doubles by using the difference to a reference date. Additionally you need to implement how to display such a translated value into a time/date string - f.e. for the axis tick labels.

Unfortunately Qwt doesn't offer a scale engine, that calculates useful scale ticks for date/time intervals. So if you want to have ticks according to something like 60/60/24 or 7/52/365 you need to set the ticks manually - otherwise you get ticks based on decades.

This is what the friedberg example does - and what doesn't work very well when you zoom in.

Uwe

falconium
1st May 2011, 06:07
I found this example hidden in the code as commented, but it cannot be compiled. This looks to be a simple application of Qwt, and it is almost what I need to start with and to learn some basics.


#include <cmath>
#include <qwt_series_data.h>
#include <qwt_plot_curve.h>
#include <qwt_plot.h>
#include <qapplication.h>

class SinusData: public QwtSyntheticPointData
{
public:
SinusData():
QwtSyntheticPointData(100)
{
}
virtual double y(double x) const
{
return qSin(x);
}
};

int main(int argc, char **argv)
{
QApplication a(argc, argv);

QwtPlot plot;
plot.setAxisScale(QwtPlot::xBottom, 0.0, 10.0);
plot.setAxisScale(QwtPlot::yLeft, -1.0, 1.0);

QwtPlotCurve *curve = new QwtPlotCurve("y = sin(x)");
curve->setData(SinusData());
curve->attach(&plot);

plot.show();
return a.exec();
}


This is what I get as error:

..\sinus\main.cpp: In function 'int qMain(int, char**)':
..\sinus\main.cpp:29: error: no matching function for call to 'QwtPlotCurve::setData(SinusData)'
..\sinus\libraries\qwt/qwt_plot_seriesitem.h:146: note: candidates are: void QwtPlotSeriesItem<T>::setData(QwtSeriesData<T>*) [with T = QPointF]
mingw32-make[1]: *** [release/main.o] Error 1

If you can fix this code for me, I think I will be able to extend it with more. Thanx in advance!

Uwe
1st May 2011, 07:14
This is basic C++ and the compiler already told you what to do: "curve->setData( new SinusData() );"

In most applications you want to pass samples from some sort array instead of a data object, that calculates values on the fly. So check all classes derived from QwtSeriesData and the convenience methods QwtPlotCurve::setSamples().

Uwe

falconium
1st May 2011, 18:06
Yes, it was the problem. I thought that it could be some mismatch in arguments, but I also noticed finally that not the instance is given. Thx!

falconium
2nd May 2011, 05:24
Alright, this is where I got:


#include <qapplication.h>
#include <qlayout.h>
#include <qlabel.h>
#include <qtime>
#include <qpainter.h>
#include <qwt_plot_layout.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_scale_draw.h>
#include <qwt_scale_widget.h>
#include <qwt_legend.h>
#include <qwt_legend_item.h>
#include <qwt_curve_fitter.h>
#include "chartplot.h"
#include <QDebug>

class Grid: public QwtPlotGrid
{
public:
Grid()
{
enableXMin(true);
setMajPen(QPen(Qt::white, 0, Qt::DotLine));
setMinPen(QPen(Qt::lightGray, 0 , Qt::DotLine));
}

virtual void updateScaleDiv(const QwtScaleDiv &xMap,
const QwtScaleDiv &yMap)
{
QList<double> ticks[QwtScaleDiv::NTickTypes];

ticks[QwtScaleDiv::MajorTick] = xMap.ticks(QwtScaleDiv::MajorTick);
ticks[QwtScaleDiv::MinorTick] = xMap.ticks(QwtScaleDiv::MinorTick);

QwtPlotGrid::updateScaleDiv(
QwtScaleDiv(xMap.lowerBound(), xMap.upperBound(), ticks),
yMap );
}
};

class TimeScaleDraw: public QwtScaleDraw
{
public:
TimeScaleDraw(/*const QTime &base*/)/* : baseTime(base)*/
{
setTickLength(QwtScaleDiv::MajorTick, 6);
setTickLength(QwtScaleDiv::MinorTick, 0);
setTickLength(QwtScaleDiv::MediumTick, 0);
setLabelRotation(0);
setLabelAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
setSpacing(20);
}

virtual QwtText label(double v) const
{
// QTime upTime = baseTime.addSecs((int)v);
QDateTime time;
time = time.fromTime_t(v);
return time.toString("yyyy.MM.dd.\nhh:mm:ss");
}
//private:
// QTime baseTime;
};

class Background: public QwtPlotItem
{
public:
Background()
{
setZ(0.0);
}

virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}

virtual void draw(QPainter *painter,
const QwtScaleMap &, const QwtScaleMap &/*yMap*/,
const QRectF &rect) const
{
QColor c(Qt::gray);
c=c.darker(150);
// QRectF r = rect;

// r.setBottom(yMap.transform(500));
// r.setTop(yMap.transform(0));
painter->fillRect(rect, c);
}
};

class ChartCurve: public QwtPlotCurve
{
public:
ChartCurve(const QString &title) : QwtPlotCurve(title)
{
setRenderHint(QwtPlotItem::RenderAntialiased);
}

void setColor(const QColor &color)
{
QColor c = color;
c.setAlpha(150);

setPen(c);
setBrush(c);
}
};

ChartPlot::ChartPlot(QStandardItemModel *model, QWidget *parent) : QwtPlot(parent)
{
setAutoReplot(false);

plotLayout()->setAlignCanvasToScales(true);

QwtLegend *legend = new QwtLegend;
legend->setItemMode(QwtLegend::CheckableItem);
insertLegend(legend, QwtPlot::RightLegend);

setAxisTitle(QwtPlot::xBottom, "Report time");
setAxisAutoScale(QwtPlot::xBottom);
setAxisLabelRotation(QwtPlot::xBottom, -45.0);
setAxisLabelAlignment(QwtPlot::xBottom, Qt::AlignLeft | Qt::AlignBottom);
setAxisScaleDraw(QwtPlot::xBottom, new TimeScaleDraw());

QwtScaleWidget *scaleWidget = axisWidget(QwtPlot::xBottom);
const int fmh = QFontMetrics(scaleWidget->font()).height();
scaleWidget->setMinBorderDist(0, fmh / 2);

setAxisTitle(QwtPlot::yLeft, "Quantity");
//setAxisScale(QwtPlot::yLeft, 0, 50);

QwtPlotGrid *grid = new Grid;
grid->attach(this);

Background *bg = new Background();
bg->attach(this);

for (int column = 1; column < model->columnCount(); column++)
{
curve = new ChartCurve(model->horizontalHeaderItem(column)->text());
curve->setStyle(QwtPlotCurve::Lines);
QVariant decoration = model->item(0, column)->data(Qt::DecorationRole);
if (decoration.type() == QVariant::Color)
{
curve->setColor(decoration.value<QColor>());
}
curve->setCurveAttribute(QwtPlotCurve::Fitted, true);
QwtSplineCurveFitter *curveFitter = new QwtSplineCurveFitter();
curve->setCurveFitter(curveFitter);
curve->setZ(curve->z() - 1);
curve->attach(this);

QVector<double> xData;
QVector<double> yData;
for (int row=0; row<model->rowCount(); row++)
{
QDateTime xDateTime = model->item(row, 0)->data(Qt::EditRole).toDateTime();
xData << xDateTime.toTime_t();
yData << model->item(row, column)->data(Qt::EditRole).toDouble();
}
curve->setSamples(xData, yData);
showCurve(curve, true);
}

connect(this, SIGNAL(legendChecked(QwtPlotItem *, bool)), SLOT(showCurve(QwtPlotItem *, bool)));
}

void ChartPlot::showCurve(QwtPlotItem *item, bool on)
{
item->setVisible(on);
QWidget *w = legend()->find(item);
if (w && w->inherits("QwtLegendItem")) ((QwtLegendItem *)w)->setChecked(on);

replot();
}


How can I change the values of (major) ticks on axis X to match the date of source values or to match some nice value to be easily read by humans?

I also have problems with spine, as it doesn't always match precisely. The plot can grow higher than the actual value. I'll send you picture tomorrow. Here it is: wrong spline (http://www.picvalley.net/v.php?p=u/2742/211659532614688111341304310821p2AxLNhMRYOeEtJW06wK .PNG)

Pictures:

Source data: link (http://www.picvalley.net/v.php?p=u/2682/133259681911632145771304309983KlQR4uO1QiGFgsPwshD0 .PNG)
Chart: link (http://www.picvalley.net/v.php?p=u/2920/18563037588083538761304309989iBf8uPV8MRd5AWZ7qCJs. PNG)

Thanks for any idea!

falconium
2nd May 2011, 21:45
OK, I have managed to fill the gaps in the chart by using Floating attribute:


QwtLinearScaleEngine *scaleEngineX = new QwtLinearScaleEngine;
scaleEngineX->setAttribute(QwtScaleEngine::Floating, true);
setAxisScaleEngine(QwtPlot::xBottom, scaleEngineX);


Now, the question is still, how to set major ticks to exact values of the original, source data, not some intermediate, calculated ones. (And of course, if there are too many discrete values on axis, to show only part of them based on an algorithm.)

I have tried to play around with:

setAxisMaxMinor(QwtPlot::xBottom, <some value>);
setAxisMaxMajor(QwtPlot::xBottom, <model->rowCount() or other values>);

No success. They always put ticks to wrong positions.
Thank you for the help in advance! This would be the only critical thing to me.

falconium
3rd May 2011, 18:20
Now I know i would have had to use QwtScaleDiv class for this purpose:


QwtScaleDiv ChartPlot::scaleDiv()
{
QList<double> ticks[QwtScaleDiv::NTickTypes];

QList<double> &majorTicks = ticks[QwtScaleDiv::MajorTick];

foreach (double val, xData)
majorTicks += val;

QList<double> &mediumTicks = ticks[QwtScaleDiv::MediumTick];
for (int i = 0; i < xData.count() - 1; ++i)
mediumTicks += QDateTime::fromTime_t(xData[i]).addSecs(900).toTime_t();

QList<double> &minorTicks = ticks[QwtScaleDiv::MinorTick];
for (int i = 0; i < xData.count() - 1; ++i)
minorTicks += QDateTime::fromTime_t(xData[i]).addSecs(300).toTime_t();

return QwtScaleDiv(xData.first(), xData.last(), ticks);
}

Simply call this function to set axis scale divisions in your QwtPlot instance as following:


setAxisScaleDiv(QwtPlot::xBottom, scaleDiv());

The only problem now I have is to set one minor tick to 1/10 of major tick and one medium tick to 1/5 of major tick. Thanks for suggestions in advance!

falconium
3rd May 2011, 21:21
I solved it, please, find it here for future reference:


QwtScaleDiv ChartPlot::scaleDiv()
{
QList<double> ticks[QwtScaleDiv::NTickTypes];

QList<double> &majorTicks = ticks[QwtScaleDiv::MajorTick];

foreach (double val, xData)
majorTicks += val;

if (xData.count()>1)
{
QList<double> &mediumTicks = ticks[QwtScaleDiv::MediumTick];
for (int i = 0; i < xData.count() - 1; i++)
for (int j = 1; j < 6; j++)
mediumTicks += xData[i] + j * (xData[i+1] - xData[i]) / 5;

QList<double> &minorTicks = ticks[QwtScaleDiv::MinorTick];
for (int i = 0; i < xData.count()*10 - 1; i++)
for (int j = 1; j < 11; j++)
minorTicks += xData[i] + j * (xData[i+1] - xData[i]) / 10;
}

return QwtScaleDiv(xData.first(), xData.last(), ticks);
}