QCustomPlot Discussion and Comments

Plotting time on a graph's y-axisReturn to overview

I'm struggling to print runtime on a graph's y-axis.

I have a time date string which is specified as hh:mm:ss. I convert this to milli seconds before passing the values to QCP:

int runtime = 0;
QTime time = QTime::fromString("00:00:18","hh:mm:ss");
runtime += time.hour() * 3600;
runtime += time.minute() * 60;
runtime += time.second();
double runtime_dbl = (double) runtime;

I've verified that the resulting runtime_dbl is correct. I pass this value to a QCPBars plottable and set up my y-axis like this:

yAxis->setTickLabelType(QCPAxis::ltDateTime);
yAxis->setDateTimeFormat("hh:mm:ss");

When I plot this, the time always shows 8 hours + the correct number of seconds (for my current dataset at least). For the above example, the y-axis shows "08:00:18".

Any ideas on what I'm doing wrong here?
Thanks
Jaco

Does your timezone happen to be +/-8?

Yes you are correct, my timezone is GMT +8. Is it possible to make the axis timezone independent?

You have to correctly carry the time zone information. This basically works two ways:

You can just let QDateTime do all the work, and just use the toMSecsSinceEpoch function. It automatically takes care that the time zones are interpreted correctly (since internally, QCustomPlot again calls fromMSecsSinceEpoch).

  QDateTime time = QDateTime::fromString("00:00:18","hh:mm:ss");
  double runtime_dbl = time.toMSecsSinceEpoch()/1000.0;
  ...

If you want to do the conversion to seconds yourself as in your example, make sure you use the UTC time to do that. This is necessary because the time reference point 0 corresponds to 01.01.1970-00:00:00 UTC. You could do that like this:

  int runtime = 0;
  QDateTime time = QDateTime::fromString("00:00:18","hh:mm:ss");
  time = time.toUTC();
  runtime += time.time().hour() * 3600;
  runtime += time.time().minute() * 60;
  runtime += time.time().second();
  double runtime_dbl = (double) runtime;
  ...

Let me know if that clears things up.

Thanks!

Unfortunately neither of the suggestions worked.

The first shows funny times where the higher values are on the bottom of the y-axis compared to lower values being on the top of the y-axis. I've printed the date time and it seems correct. For example, "Mon Jan 1 00:00:46 1900" for 46 seconds. Calling toMSecsSinceEpoch()/1000.0 on that QDateTime object results in the funny values.

The second solution does not make any difference. All times still have a +8 hour offset.

Any idea what's going wrong here? Thanks!

That is very peculiar. Here's a simple demo of what goes on outside and inside QCP:

  QDateTime time = QDateTime::fromString("00:00:05","hh:mm:ss");
  double timeValue1 = time.toMSecsSinceEpoch()/1000.0;  
  double timeValue2 = timeValue1 + 90*60; // add 1 hour 30 minutes
  
  // this is approximately what QCP does internally to generate date/time tick labels:
  QString tickLabel1 = QDateTime::fromMSecsSinceEpoch(timeValue1*1000).toString("hh:mm:ss");
  QString tickLabel2 = QDateTime::fromMSecsSinceEpoch(timeValue2*1000).toString("hh:mm:ss");
  qDebug() << tickLabel1 << tickLabel2;

And on my system (UTC+1), it correctly prints
"00:00:05" "01:30:05"


Now to the other variant that I mentioned:

  QDateTime time = QDateTime::fromString("00:00:05","hh:mm:ss");
  time = time.toUTC();
  double timeValue1 = time.time().second() + time.time().minute()*60 + time.time().hour()*3600;  
  double timeValue2 = timeValue1 + 90*60; // add 1 hour 30 minutes
  
  // this is approximately what QCP does internally to generate date/time tick labels:
  QString tickLabel1 = QDateTime::fromMSecsSinceEpoch(timeValue1*1000).toString("hh:mm:ss");
  QString tickLabel2 = QDateTime::fromMSecsSinceEpoch(timeValue2*1000).toString("hh:mm:ss");
  qDebug() << tickLabel1 << tickLabel2;

it, too, prints the above debug output.

And if I comment out the time = time.toUTC();, it prints as expected the "wrong" times:
"01:00:05" "02:30:05"

Is this different on your system? I've tested this on linux, Qt 4.7.4, 4.8.3, 5.0.2

Hi again,

I created an example that demonstrates the problem. I added an additional plot to the plot-examples example. The cpp implementation is below so you can just add the function definition in the header and extend your case statement in setupDemo() to use it.

There is a boolean variable called use_toUtc_approach which you can toggle to switch between the two solutions you proposed.

This example fails for us (note that it fails differently depending on the value of use_toUtc_approach). We are testing on Qt 5.1, Win7 & Ubuntu & CentOs (same behavior everywhere). Can you please check if this works for you?

Thanks,
Jaco

void MainWindow::setupTimeDemo(QCustomPlot *customPlot)
{
    demoName = "Time Demo";

    QCPBars* bars = new QCPBars(customPlot->xAxis, customPlot->yAxis);
    customPlot->addPlottable(bars);

    QVector<double> ticks, y0;
    ticks << 1 << 2;
    QVector<QString> labels;
    labels << "Point 1" << "Point 2";

    bool use_toUtc_approach = false;
    double runtime_dbl;
    int runtime = 0;

    // Datapoint 1 - 00:00:48
    QDateTime time1 = QDateTime::fromString("00:00:48","hh:mm:ss");
    if (use_toUtc_approach) {
        time1.toUTC();
        qDebug() << "time1" << time1.toString();
        runtime += time1.time().hour() * 3600;
        runtime += time1.time().minute() * 60;
        runtime += time1.time().second();
        runtime_dbl = (double) runtime;

        y0 << runtime_dbl;
    } else {
        qDebug() << "time1" << time1.toString();
        runtime_dbl = time1.toMSecsSinceEpoch()/1000.0;
        y0 << runtime_dbl;
    }

    // Datapoint 1 - 00:05:36
    runtime = 0;
    QDateTime time2 = QDateTime::fromString("00:05:36","hh:mm:ss");
    if (use_toUtc_approach) {
        time2.toUTC();
        qDebug() << "time2" << time2.toString();
        runtime += time2.time().hour() * 3600;
        runtime += time2.time().minute() * 60;
        runtime += time2.time().second();
        runtime_dbl = (double) runtime;

        y0 << runtime_dbl;
    } else {
        qDebug() << "time2" << time2.toString();
        runtime_dbl = time2.toMSecsSinceEpoch()/1000.0;
        y0 << runtime_dbl;
    }

    // Setup X-Axis
    customPlot->xAxis->setTickVectorLabels(labels);
    customPlot->xAxis->setAutoTicks(false);
    customPlot->xAxis->setAutoTickLabels(false);
    customPlot->xAxis->setTickLabelRotation(-60);
    customPlot->xAxis->setSubTickCount(0);
    customPlot->xAxis->setTickLength(0, 4);
    customPlot->xAxis->grid()->setVisible(true);
    customPlot->xAxis->setRange(0, ticks.count()+1);
    customPlot->xAxis->setTickVector(ticks);

    // Setup Y-Axis
    customPlot->yAxis->setRange(0, runtime_dbl+1);
    customPlot->yAxis->setTickLabelType(QCPAxis::ltDateTime);
    customPlot->yAxis->setDateTimeFormat("hh:mm:ss");

    bars->setData(ticks, y0);
}

Hi Jaco,

A small observation first: in line 20 and 38 you use the method toUTC() in a way that has no effect. It doesn't change the current object, but returns a changed datetime, so you should write time1 = time1.toUTC(); instead.

Now, here's the code how it works with using the method where toMSecsSinceEpoch is used to generate the times:

  demoName = "Time Demo";
  
  QCPBars* bars = new QCPBars(customPlot->xAxis, customPlot->yAxis);
  customPlot->addPlottable(bars);
  
  QVector<double> ticks, y0;
  ticks << 1 << 2;
  QVector<QString> labels;
  labels << "Point 1" << "Point 2";
  
  double runtime_dbl;
  
  // Datapoint 1 - 00:00:48
  QDateTime time1 = QDateTime::fromString("00:00:48 01.01.1970","hh:mm:ss dd.MM.yyyy");
  time1.setTimeSpec(Qt::UTC);
  runtime_dbl = time1.toMSecsSinceEpoch()/1000.0;
  y0 << runtime_dbl;
  qDebug() << runtime_dbl << QDateTime::fromMSecsSinceEpoch(runtime_dbl*1000).toString("hh:mm:ss dd.MM.yyyy");
  
  // Datapoint 1 - 00:05:36
  QDateTime time2 = QDateTime::fromString("00:05:36 01.01.1970","hh:mm:ss dd.MM.yyyy");
  time2.setTimeSpec(Qt::UTC);
  runtime_dbl = time2.toMSecsSinceEpoch()/1000.0;
  y0 << runtime_dbl;
  qDebug() << runtime_dbl << QDateTime::fromMSecsSinceEpoch(runtime_dbl*1000).toString("hh:mm:ss dd.MM.yyyy");
  
  // Setup X-Axis
  customPlot->xAxis->setTickVectorLabels(labels);
  customPlot->xAxis->setAutoTicks(false);
  customPlot->xAxis->setAutoTickLabels(false);
  customPlot->xAxis->setTickLabelRotation(-60);
  customPlot->xAxis->setSubTickCount(0);
  customPlot->xAxis->setTickLength(0, 4);
  customPlot->xAxis->grid()->setVisible(true);
  customPlot->xAxis->setRange(0, ticks.count()+1);
  customPlot->xAxis->setTickVector(ticks);
  
  // Setup Y-Axis
  customPlot->yAxis->setRange(0, runtime_dbl+1);
  customPlot->yAxis->setTickLabelType(QCPAxis::ltDateTime);
  customPlot->yAxis->setDateTimeSpec(Qt::UTC);
  customPlot->yAxis->setDateTimeFormat("hh:mm:ss dd.MM.yyyy");
  
  bars->setData(ticks, y0);

We completely stay in UTC time. This is necessary because the bars plottable anchors the base of the bar at value 0, which only in UTC is 00:00:00. If interpreted in local time, the bars will have their base at your local timezone offset and eventually generate upside down bars. To completely stay in UTC we also need to use QCPAxis::setDateTimeSpec(Qt::UTC) in line 41 which is new in QCP 1.1.0.

Now there's another pitfall that's worth mentioning: Passing just a time like "00:05:36" to QDateTime::fromString defaults to the date 01.01.1900. This is not the UTC 0, which is 01.01.1970. So If we had left the date unspecified in lines 14 and 21, we would have ended up with gigantic negative bars from 1970 (the UTC 0) down to 1900. Obviously, if only viewing hh:mm:ss, this results in seemngly random tick labels. That's why I let it also print day month and year in the above example, to figure this out.

Okay, now how to do it with the explicitly constructed runtime_dbl from hour, minute and second values:

  demoName = "Time Demo";
  
  QCPBars* bars = new QCPBars(customPlot->xAxis, customPlot->yAxis);
  customPlot->addPlottable(bars);
  
  QVector<double> ticks, y0;
  ticks << 1 << 2;
  QVector<QString> labels;
  labels << "Point 1" << "Point 2";
  
  double runtime_dbl;
  int runtime = 0;
  
  // Datapoint 1 - 00:00:48
  QDateTime time1 = QDateTime::fromString("00:00:48","hh:mm:ss");
  runtime += time1.time().hour() * 3600;
  runtime += time1.time().minute() * 60;
  runtime += time1.time().second();
  runtime_dbl = (double) runtime;
  y0 << runtime_dbl;
  qDebug() << runtime_dbl << QDateTime::fromMSecsSinceEpoch(runtime_dbl*1000).toString("hh:mm:ss dd.MM.yyyy");
  
  // Datapoint 1 - 00:05:36
  runtime = 0;
  QDateTime time2 = QDateTime::fromString("00:05:36","hh:mm:ss");
  runtime += time2.time().hour() * 3600;
  runtime += time2.time().minute() * 60;
  runtime += time2.time().second();
  runtime_dbl = (double) runtime;
  y0 << runtime_dbl;
  qDebug() << runtime_dbl << QDateTime::fromMSecsSinceEpoch(runtime_dbl*1000).toString("hh:mm:ss dd.MM.yyyy");
  
  // Setup X-Axis
  customPlot->xAxis->setTickVectorLabels(labels);
  customPlot->xAxis->setAutoTicks(false);
  customPlot->xAxis->setAutoTickLabels(false);
  customPlot->xAxis->setTickLabelRotation(-60);
  customPlot->xAxis->setSubTickCount(0);
  customPlot->xAxis->setTickLength(0, 4);
  customPlot->xAxis->grid()->setVisible(true);
  customPlot->xAxis->setRange(0, ticks.count()+1);
  customPlot->xAxis->setTickVector(ticks);
  
  // Setup Y-Axis
  customPlot->yAxis->setRange(0, runtime_dbl+1);
  customPlot->yAxis->setTickLabelType(QCPAxis::ltDateTime);
  customPlot->yAxis->setDateTimeSpec(Qt::UTC);
  customPlot->yAxis->setDateTimeFormat("hh:mm:ss dd.MM.yyyy");
  
  bars->setData(ticks, y0);

There's not much difference, apart from the fact that now we can get away with not specifying the date explicitly in the QDateTime::fromString method. This is because we use only the time part of the datetime object and construct our runtime_dbl from that, effectively starting from the correct UTC 0.


The fact that the bars plottable always places its bar base at value 0 is unfortunate and will be changed in a future release. In your case, as shown above, this behaviour is okay if we set the axis timespec to be UTC, where the value 0 actually is time 00:00:00. I hope switching to QCustomPlot 1.1.0 is an option for you, because before that I'm afraid the bars plottable will always place its base at the local time offset.

Thanks for the detailed explanation and taking the time to figure this one out. I tested it on my side as well and it works perfectly after I upgraded to v1.1.

Hi everyone! I'm trying to create some plot with time on x axis, and function on the second axis. As reference I used simple Graphic example! So what's the problem, how can I extract data from QDateTime vector, and just set it into my plot (x axis)? I can't find how do that simple!

double start = times[0].toTime_t();
    double end = times.end()->toTime_t();

    //Get maximum and minimum values
    double maxAzimut = *std::max_element(azimuth.constBegin(),azimuth.constEnd());
    double minAzimut = *std::min_element(azimuth.constBegin(),azimuth.constEnd());


    QSharedPointer<QCPAxisTickerDateTime> dateTicker(new QCPAxisTickerDateTime);
    dateTicker->setDateTimeFormat("d. MMMM\nyyyy");
    ui->customPlot->addGraph();
    ui->customPlot->graph(0)->setData(azimuth, );
    ui->customPlot->xAxis->setTicker(dateTicker);
    ui->customPlot->xAxis->setLabel("time");
    ui->customPlot->yAxis->setLabel("Elevation");
    ui->customPlot->xAxis->setRange(start, end);
    ui->customPlot->yAxis->setRange(minAzimut, maxAzimut);
    ui->customPlot->replot();