QCustomPlot Discussion and Comments

QCPAxis::coordToPixel check for NaNReturn to overview

QCustomPlot 2.0.1 25.06.18
MSVS 2019 version 16.3.9
Qt 5.12.4 64-bit

I have a simple line graph that contains some gaps in data. As per instructions, I am adding std::numeric_limits<double>::quiet_NaN() as values for these points.

"Gaps in the graph line can be created by adding data points with NaN as value (qQNaN() or std::numeric_limits<double>::quiet_NaN()) in between the two data points that shall be separated."

My graph looks great when compiled in debug mode, but when compiled in release mode, the gaps are replaced with points out of the display rect. I tracked the difference to QCPAxis::coordToPixel. This method does not do a check for NaN. Instead it looks like it relies on proper handing of NaN values, which is not happening in my case. It looks like it is falling into this code:

return !mRangeReversed ? mAxisRect->bottom()+200 : mAxisRect->top()-200;

which does not use the value, so it returns something other than NaN.

I added an explicit check for NaN which seems to solve this problem.

double QCPAxis::coordToPixel(double value) const
if (qIsNaN(value)) return value;

I am getting to QCPAxis::coordToPixel through a call to QCPGraph::dataToLines through a call to QCPGraph::getLines.

This looks like the right place to add this check, and the check seems reasonable. Thoughts?

Hi David,

the NaN-Gap creation is a lot more complicated (think of segmentation of channel fills etc., see methods getNonNanSegments, getOverlappingSegments, etc.) which is why it can't be handled by such a check in coordToPixel. Further, it unnecessarily slows down that function wich is used in many places.

The actual NaN check is performed here:

template <class DataType>
void QCPAbstractPlottable1D<DataType>::drawPolyline(QCPPainter *painter, const QVector<QPointF> &lineData) const

(in the amalgamated sources, this is in qcustomplot.h, since it's a templated method)

From what you describe it seems like the Qt method qIsNaN is broken in your setup/compiler and falsely returns "false" even if you've set it to NaN before. This seems to happen on some compilers, as C++ doesn't offer proper abilities to handle and reliably check for NaNs. Maybe you could experiment with other typical NaN checks (a != a), or set some compiler flags to avoid it from messing with NaN.

For an external discussion of this flaw of the C++ language (and some compiler optimization), check here:

I hear what you are telling me, but it does look like my qIsNan is working. That is the call that I added to correct the issue that I am seeing. I do see where coordToPixel is called quite a lot. I also see the the qIsNan check in QCPAbstractPlottable1D<DataType>::drawPolyline, but when I get to here, the NaN is no longer in my line data. It has been removed when the line data is converted from world coordinates to screen coordinates in the call to getLines/dataToLines/coordToPixel. What I am seeing is a value of NaN being passed into coordToPixel and a value other than NaN being returned.

Here is the call stack when NaN is passed to coordToPixel.
ProETCH.exe!QCPAxis::coordToPixel(double value) Line 8607 C++
ProETCH.exe!QCPGraph::dataToLines(const QVector<QCPGraphData> & data) Line 20595 C++
ProETCH.exe!QCPGraph::getLines(QVector<QPointF> * lines, const QCPDataRange & dataRange) Line 20498 C++
ProETCH.exe!QCPGraph::draw(QCPPainter * painter) Line 20385 C++
ProETCH.exe!QCPLayer::draw(QCPPainter * painter) Line 1128 C++
ProETCH.exe!QCPLayer::drawToPaintBuffer() Line 1148 C++
ProETCH.exe!QCustomPlot::replot(QCustomPlot::RefreshPriority refreshPriority) Line 14448 C++

I do not see a call to qIsNan before the call to coordToPixel in this case. The call to drawPolyline comes later.

The incomming data for dataToLines is:
- data { size = 5 } const QVector<QCPGraphData> &
+ [referenced] {...} std::atomic<int>
+ [0] {key=0.00000000000000000 value=100000000000000.00 } QCPGraphData
+ [1] {key=32.500000000000000 value=100000000000000.00 } QCPGraphData
+ [2] {key=56.000000000000000 value=nan } QCPGraphData
+ [3] {key=56.000000000000000 value=100000000000000.00 } QCPGraphData
+ [4] {key=66.000000000000000 value=100000000000000.00 } QCPGraphData

The result of dataToLines is:
- result { size = 5 } QVector<QPointF>
+ [referenced] {...} std::atomic<int>
+ [0] { x = 96.681818181818173, y = -13.525343969977705 } QPointF
+ [1] { x = 73.851239669421489, y = -13.525343969977705 } QPointF
+ [2] { x = 57.342975206611584, y = 163.00000000000000 } QPointF
+ [3] { x = 57.342975206611584, y = -13.525343969977705 } QPointF
+ [4] { x = 50.318181818181827, y = -13.525343969977705 } QPointF

It looks like what is happening in coordToPixel is that it executes this code:

      if (value >= 0.0 && mRange.upper < 0.0) // invalid value for logarithmic scale, just draw it outside visible range
        return !mRangeReversed ? mAxisRect->top()-200 : mAxisRect->bottom()+200;
      else if (value <= 0.0 && mRange.upper >= 0.0) // invalid value for logarithmic scale, just draw it outside visible range
        return !mRangeReversed ? mAxisRect->bottom()+200 : mAxisRect->top()-200;
        if (!mRangeReversed)
          return mAxisRect->bottom()-qLn(value/mRange.lower)/qLn(mRange.upper/mRange.lower)*mAxisRect->height();
          return mAxisRect->bottom()-qLn(mRange.upper/value)/qLn(mRange.upper/mRange.lower)*mAxisRect->height();

It looks like the check for value <= 0.0 is INCORRECTLY evaluating to true and then this code is executed:
return !mRangeReversed ? mAxisRect->bottom()+200 : mAxisRect->top()-200;
which does not return NaN. If I use stLinear scale on the axis it falls into this code:
return (value-mRange.lower)/mRange.size()*mAxisRect->width()+mAxisRect->left();
which uses the value in the calculation and correctly returns NaN.

From what I can tell, there is not an explicit check for NaN before this call.

I agree that it looks like there is a problem with my compiler. I have updated MSVS to 16.4.2 and 16.4.3 and I am still seeing this issue. I could move the NaN check up to dataToLines, but that does not account for the other dataToXXX methods.

I did look at the link that you suggested about the problems with NaN support, but mostly that makes me wonder why NaN was choosen as the hole value.

After a bunch of trial and error and research, I found a solution to this issue. For MSVC I needed to change the Floating Point Model paramter from /fp:fast to /fp:precise. There is appearently an issue with optimization and NaN comparison. See the following discussion for more details.