##// END OF EJS Templates
Removed hack from QCustomPlot, moved to QCustomPlotVect class.
Removed hack from QCustomPlot, moved to QCustomPlotVect class.

File last commit:

r7:6e23aedd1ca8 default
r7:6e23aedd1ca8 default
Show More
qcustomplotvect.cpp
619 lines | 26.8 KiB | text/x-c | CppLexer
/ src / Core / Widgets / qcustomplotvect.cpp
Removed hack from QCustomPlot, moved to QCustomPlotVect class.
r7 #include "qcustomplotvect.h"
#include <QVector>
#include <QVectorIterator>
QCustomPlotVect::QCustomPlotVect(QWidget *parent)
:QCustomPlot(parent)
{
}
QCustomPlotVect::~QCustomPlotVect()
{
}
QCPGraphVect *QCustomPlotVect::addGraph(QCPAxis *keyAxis, QCPAxis *valueAxis)
{
if (!keyAxis) keyAxis = xAxis;
if (!valueAxis) valueAxis = yAxis;
if (!keyAxis || !valueAxis)
{
qDebug() << Q_FUNC_INFO << "can't use default QCustomPlot xAxis or yAxis, because at least one is invalid (has been deleted)";
return 0;
}
if (keyAxis->parentPlot() != this || valueAxis->parentPlot() != this)
{
qDebug() << Q_FUNC_INFO << "passed keyAxis or valueAxis doesn't have this QCustomPlot as parent";
return 0;
}
QCPGraphVect *newGraph = new QCPGraphVect(keyAxis, valueAxis);
if (addPlottable(newGraph))
{
newGraph->setName(QLatin1String("Graph ")+QString::number(mGraphs.size()));
return newGraph;
} else
{
delete newGraph;
return 0;
}
}
QCPGraphVect::QCPGraphVect(QCPAxis *keyAxis, QCPAxis *valueAxis)
:QCPGraph(keyAxis,valueAxis)
{
mData = new QVector<QCPData>();
}
QCPGraphVect::~QCPGraphVect()
{
}
void QCPGraphVect::setData(QVector<QCPData> *data)
{
if(data!=mData)
{
delete this->mData;
this->mData = data;
}
}
void QCPGraphVect::draw(QCPPainter *painter)
{
if (!mKeyAxis || !mValueAxis) { qDebug() << Q_FUNC_INFO << "invalid key or value axis"; return; }
int test = mKeyAxis.data()->range().size();
test = mData->count();
if (mKeyAxis.data()->range().size() <= 0 || mData->isEmpty()) return;
if (mLineStyle == lsNone && mScatterStyle.isNone()) return;
// allocate line and (if necessary) point vectors:
QVector<QPointF> *lineData = new QVector<QPointF>;
QVector<QCPData> *scatterData = 0;
if (!mScatterStyle.isNone())
scatterData = new QVector<QCPData>;
// fill vectors with data appropriate to plot style:
getPlotData(lineData, scatterData);
// check data validity if flag set:
#ifdef QCUSTOMPLOT_CHECK_DATA
QCPDataMap::const_iterator it;
for (it = mData->constBegin(); it != mData->constEnd(); ++it)
{
if (QCP::isInvalidData(it.value().key, it.value().value) ||
QCP::isInvalidData(it.value().keyErrorPlus, it.value().keyErrorMinus) ||
QCP::isInvalidData(it.value().valueErrorPlus, it.value().valueErrorPlus))
qDebug() << Q_FUNC_INFO << "Data point at" << it.key() << "invalid." << "Plottable name:" << name();
}
#endif
// draw fill of graph:
drawFill(painter, lineData);
// draw line:
if (mLineStyle == lsImpulse)
drawImpulsePlot(painter, lineData);
else if (mLineStyle != lsNone)
drawLinePlot(painter, lineData); // also step plots can be drawn as a line plot
// draw scatters:
if (scatterData)
drawScatterPlot(painter, scatterData);
// free allocated line and point vectors:
delete lineData;
if (scatterData)
delete scatterData;
}
void QCPGraphVect::getPlotData(QVector<QPointF> *lineData, QVector<QCPData> *scatterData) const
{
switch(mLineStyle)
{
case lsNone: getScatterPlotData(scatterData); break;
case lsLine: getLinePlotData(lineData, scatterData); break;
case lsStepLeft: getStepLeftPlotData(lineData, scatterData); break;
case lsStepRight: getStepRightPlotData(lineData, scatterData); break;
case lsStepCenter: getStepCenterPlotData(lineData, scatterData); break;
case lsImpulse: getImpulsePlotData(lineData, scatterData); break;
}
}
void QCPGraphVect::getLinePlotData(QVector<QPointF> *linePixelData, QVector<QCPData> *scatterData) const
{
QCPAxis *keyAxis = mKeyAxis.data();
QCPAxis *valueAxis = mValueAxis.data();
if (!keyAxis || !valueAxis) { qDebug() << Q_FUNC_INFO << "invalid key or value axis"; return; }
if (!linePixelData) { qDebug() << Q_FUNC_INFO << "null pointer passed as linePixelData"; return; }
QVector<QCPData> lineData;
getPreparedData(&lineData, scatterData);
linePixelData->reserve(lineData.size()+2); // added 2 to reserve memory for lower/upper fill base points that might be needed for fill
linePixelData->resize(lineData.size());
// transform lineData points to pixels:
if (keyAxis->orientation() == Qt::Vertical)
{
for (int i=0; i<lineData.size(); ++i)
{
(*linePixelData)[i].setX(valueAxis->coordToPixel(lineData.at(i).value));
(*linePixelData)[i].setY(keyAxis->coordToPixel(lineData.at(i).key));
}
} else // key axis is horizontal
{
for (int i=0; i<lineData.size(); ++i)
{
(*linePixelData)[i].setX(keyAxis->coordToPixel(lineData.at(i).key));
(*linePixelData)[i].setY(valueAxis->coordToPixel(lineData.at(i).value));
}
}
}
QCPRange QCPGraphVect::getKeyRange(bool &foundRange, QCPAbstractPlottable::SignDomain inSignDomain, bool includeErrors) const
{
QCPRange range;
bool haveLower = false;
bool haveUpper = false;
double current, currentErrorMinus, currentErrorPlus;
if (inSignDomain == sdBoth) // range may be anywhere
{
QVector<QCPData>::const_iterator it = mData->constBegin();
while (it != mData->constEnd())
{
current = (*it).key;
currentErrorMinus = (includeErrors ? (*it).keyErrorMinus : 0);
currentErrorPlus = (includeErrors ? (*it).keyErrorPlus : 0);
if (current-currentErrorMinus < range.lower || !haveLower)
{
range.lower = current-currentErrorMinus;
haveLower = true;
}
if (current+currentErrorPlus > range.upper || !haveUpper)
{
range.upper = current+currentErrorPlus;
haveUpper = true;
}
++it;
}
} else if (inSignDomain == sdNegative) // range may only be in the negative sign domain
{
QVector<QCPData>::const_iterator it = mData->constBegin();
while (it != mData->constEnd())
{
current = (*it).key;
currentErrorMinus = (includeErrors ? (*it).keyErrorMinus : 0);
currentErrorPlus = (includeErrors ? (*it).keyErrorPlus : 0);
if ((current-currentErrorMinus < range.lower || !haveLower) && current-currentErrorMinus < 0)
{
range.lower = current-currentErrorMinus;
haveLower = true;
}
if ((current+currentErrorPlus > range.upper || !haveUpper) && current+currentErrorPlus < 0)
{
range.upper = current+currentErrorPlus;
haveUpper = true;
}
if (includeErrors) // in case point is in valid sign domain but errobars stretch beyond it, we still want to geht that point.
{
if ((current < range.lower || !haveLower) && current < 0)
{
range.lower = current;
haveLower = true;
}
if ((current > range.upper || !haveUpper) && current < 0)
{
range.upper = current;
haveUpper = true;
}
}
++it;
}
} else if (inSignDomain == sdPositive) // range may only be in the positive sign domain
{
QVector<QCPData>::const_iterator it = mData->constBegin();
while (it != mData->constEnd())
{
current = (*it).key;
currentErrorMinus = (includeErrors ? (*it).keyErrorMinus : 0);
currentErrorPlus = (includeErrors ? (*it).keyErrorPlus : 0);
if ((current-currentErrorMinus < range.lower || !haveLower) && current-currentErrorMinus > 0)
{
range.lower = current-currentErrorMinus;
haveLower = true;
}
if ((current+currentErrorPlus > range.upper || !haveUpper) && current+currentErrorPlus > 0)
{
range.upper = current+currentErrorPlus;
haveUpper = true;
}
if (includeErrors) // in case point is in valid sign domain but errobars stretch beyond it, we still want to get that point.
{
if ((current < range.lower || !haveLower) && current > 0)
{
range.lower = current;
haveLower = true;
}
if ((current > range.upper || !haveUpper) && current > 0)
{
range.upper = current;
haveUpper = true;
}
}
++it;
}
}
foundRange = haveLower && haveUpper;
return range;
}
QCPRange QCPGraphVect::getValueRange(bool &foundRange, QCPAbstractPlottable::SignDomain inSignDomain, bool includeErrors) const
{
QCPRange range;
bool haveLower = false;
bool haveUpper = false;
double current, currentErrorMinus, currentErrorPlus;
if (inSignDomain == sdBoth) // range may be anywhere
{
QVector<QCPData>::const_iterator it = mData->constBegin();
while (it != mData->constEnd())
{
current = (*it).value;
currentErrorMinus = (includeErrors ? (*it).valueErrorMinus : 0);
currentErrorPlus = (includeErrors ? (*it).valueErrorPlus : 0);
if (current-currentErrorMinus < range.lower || !haveLower)
{
range.lower = current-currentErrorMinus;
haveLower = true;
}
if (current+currentErrorPlus > range.upper || !haveUpper)
{
range.upper = current+currentErrorPlus;
haveUpper = true;
}
++it;
}
} else if (inSignDomain == sdNegative) // range may only be in the negative sign domain
{
QVector<QCPData>::const_iterator it = mData->constBegin();
while (it != mData->constEnd())
{
current = (*it).value;
currentErrorMinus = (includeErrors ? (*it).valueErrorMinus : 0);
currentErrorPlus = (includeErrors ? (*it).valueErrorPlus : 0);
if ((current-currentErrorMinus < range.lower || !haveLower) && current-currentErrorMinus < 0)
{
range.lower = current-currentErrorMinus;
haveLower = true;
}
if ((current+currentErrorPlus > range.upper || !haveUpper) && current+currentErrorPlus < 0)
{
range.upper = current+currentErrorPlus;
haveUpper = true;
}
if (includeErrors) // in case point is in valid sign domain but errobars stretch beyond it, we still want to get that point.
{
if ((current < range.lower || !haveLower) && current < 0)
{
range.lower = current;
haveLower = true;
}
if ((current > range.upper || !haveUpper) && current < 0)
{
range.upper = current;
haveUpper = true;
}
}
++it;
}
} else if (inSignDomain == sdPositive) // range may only be in the positive sign domain
{
QVector<QCPData>::const_iterator it = mData->constBegin();
while (it != mData->constEnd())
{
current = (*it).value;
currentErrorMinus = (includeErrors ? (*it).valueErrorMinus : 0);
currentErrorPlus = (includeErrors ? (*it).valueErrorPlus : 0);
if ((current-currentErrorMinus < range.lower || !haveLower) && current-currentErrorMinus > 0)
{
range.lower = current-currentErrorMinus;
haveLower = true;
}
if ((current+currentErrorPlus > range.upper || !haveUpper) && current+currentErrorPlus > 0)
{
range.upper = current+currentErrorPlus;
haveUpper = true;
}
if (includeErrors) // in case point is in valid sign domain but errobars stretch beyond it, we still want to geht that point.
{
if ((current < range.lower || !haveLower) && current > 0)
{
range.lower = current;
haveLower = true;
}
if ((current > range.upper || !haveUpper) && current > 0)
{
range.upper = current;
haveUpper = true;
}
}
++it;
}
}
foundRange = haveLower && haveUpper;
return range;
}
void QCPGraphVect::getPreparedData(QVector<QCPData> *lineData, QVector<QCPData> *scatterData) const
{
QCPAxis *keyAxis = mKeyAxis.data();
QCPAxis *valueAxis = mValueAxis.data();
if (!keyAxis || !valueAxis) { qDebug() << Q_FUNC_INFO << "invalid key or value axis"; return; }
// get visible data range:
QVector<QCPData>::const_iterator lower, upper; // note that upper is the actual upper point, and not 1 step after the upper point
getVisibleDataBounds(lower, upper);
if (lower == mData->constEnd() || upper == mData->constEnd())
return;
// count points in visible range, taking into account that we only need to count to the limit maxCount if using adaptive sampling:
int maxCount = std::numeric_limits<int>::max();
if (mAdaptiveSampling)
{
int keyPixelSpan = qAbs(keyAxis->coordToPixel((*lower).key)-keyAxis->coordToPixel((*upper).key));
maxCount = 2*keyPixelSpan+2;
}
int dataCount = countDataInBounds(lower, upper, maxCount);
if (mAdaptiveSampling && dataCount >= maxCount) // use adaptive sampling only if there are at least two points per pixel on average
{
if (lineData)
{
QVector<QCPData>::const_iterator it = lower;
QVector<QCPData>::const_iterator upperEnd = upper+1;
double minValue = (*it).value;
double maxValue = (*it).value;
QVector<QCPData>::const_iterator currentIntervalFirstPoint = it;
int reversedFactor = keyAxis->rangeReversed() != (keyAxis->orientation()==Qt::Vertical) ? -1 : 1; // is used to calculate keyEpsilon pixel into the correct direction
int reversedRound = keyAxis->rangeReversed() != (keyAxis->orientation()==Qt::Vertical) ? 1 : 0; // is used to switch between floor (normal) and ceil (reversed) rounding of currentIntervalStartKey
double currentIntervalStartKey = keyAxis->pixelToCoord((int)(keyAxis->coordToPixel((*lower).key)+reversedRound));
double lastIntervalEndKey = currentIntervalStartKey;
double keyEpsilon = qAbs(currentIntervalStartKey-keyAxis->pixelToCoord(keyAxis->coordToPixel(currentIntervalStartKey)+1.0*reversedFactor)); // interval of one pixel on screen when mapped to plot key coordinates
bool keyEpsilonVariable = keyAxis->scaleType() == QCPAxis::stLogarithmic; // indicates whether keyEpsilon needs to be updated after every interval (for log axes)
int intervalDataCount = 1;
++it; // advance iterator to second data point because adaptive sampling works in 1 point retrospect
while (it != upperEnd)
{
if ((*it).key < currentIntervalStartKey+keyEpsilon) // data point is still within same pixel, so skip it and expand value span of this cluster if necessary
{
if ((*it).value < minValue)
minValue = (*it).value;
else if ((*it).value > maxValue)
maxValue = (*it).value;
++intervalDataCount;
} else // new pixel interval started
{
if (intervalDataCount >= 2) // last pixel had multiple data points, consolidate them to a cluster
{
if (lastIntervalEndKey < currentIntervalStartKey-keyEpsilon) // last point is further away, so first point of this cluster must be at a real data point
lineData->append(QCPData(currentIntervalStartKey+keyEpsilon*0.2, (*currentIntervalFirstPoint).value));
lineData->append(QCPData(currentIntervalStartKey+keyEpsilon*0.25, minValue));
lineData->append(QCPData(currentIntervalStartKey+keyEpsilon*0.75, maxValue));
if ((*it).key > currentIntervalStartKey+keyEpsilon*2) // new pixel started further away from previous cluster, so make sure the last point of the cluster is at a real data point
lineData->append(QCPData(currentIntervalStartKey+keyEpsilon*0.8, (*(it-1)).value));
} else
lineData->append(QCPData((*currentIntervalFirstPoint).key, (*currentIntervalFirstPoint).value));
lastIntervalEndKey = (*(it-1)).key;
minValue = (*it).value;
maxValue = (*it).value;
currentIntervalFirstPoint = it;
currentIntervalStartKey = keyAxis->pixelToCoord((int)(keyAxis->coordToPixel((*it).key)+reversedRound));
if (keyEpsilonVariable)
keyEpsilon = qAbs(currentIntervalStartKey-keyAxis->pixelToCoord(keyAxis->coordToPixel(currentIntervalStartKey)+1.0*reversedFactor));
intervalDataCount = 1;
}
++it;
}
// handle last interval:
if (intervalDataCount >= 2) // last pixel had multiple data points, consolidate them to a cluster
{
if (lastIntervalEndKey < currentIntervalStartKey-keyEpsilon) // last point wasn't a cluster, so first point of this cluster must be at a real data point
lineData->append(QCPData(currentIntervalStartKey+keyEpsilon*0.2, (*currentIntervalFirstPoint).value));
lineData->append(QCPData(currentIntervalStartKey+keyEpsilon*0.25, minValue));
lineData->append(QCPData(currentIntervalStartKey+keyEpsilon*0.75, maxValue));
} else
lineData->append(QCPData((*currentIntervalFirstPoint).key, (*currentIntervalFirstPoint).value));
}
if (scatterData)
{
double valueMaxRange = valueAxis->range().upper;
double valueMinRange = valueAxis->range().lower;
QVector<QCPData>::const_iterator it = lower;
QVector<QCPData>::const_iterator upperEnd = upper+1;
double minValue = (*it).value;
double maxValue = (*it).value;
QVector<QCPData>::const_iterator minValueIt = it;
QVector<QCPData>::const_iterator maxValueIt = it;
QVector<QCPData>::const_iterator currentIntervalStart = it;
int reversedFactor = keyAxis->rangeReversed() ? -1 : 1; // is used to calculate keyEpsilon pixel into the correct direction
int reversedRound = keyAxis->rangeReversed() ? 1 : 0; // is used to switch between floor (normal) and ceil (reversed) rounding of currentIntervalStartKey
double currentIntervalStartKey = keyAxis->pixelToCoord((int)(keyAxis->coordToPixel((*lower).key)+reversedRound));
double keyEpsilon = qAbs(currentIntervalStartKey-keyAxis->pixelToCoord(keyAxis->coordToPixel(currentIntervalStartKey)+1.0*reversedFactor)); // interval of one pixel on screen when mapped to plot key coordinates
bool keyEpsilonVariable = keyAxis->scaleType() == QCPAxis::stLogarithmic; // indicates whether keyEpsilon needs to be updated after every interval (for log axes)
int intervalDataCount = 1;
++it; // advance iterator to second data point because adaptive sampling works in 1 point retrospect
while (it != upperEnd)
{
if ((*it).key < currentIntervalStartKey+keyEpsilon) // data point is still within same pixel, so skip it and expand value span of this pixel if necessary
{
if ((*it).value < minValue && (*it).value > valueMinRange && (*it).value < valueMaxRange)
{
minValue = (*it).value;
minValueIt = it;
} else if ((*it).value > maxValue && (*it).value > valueMinRange && (*it).value < valueMaxRange)
{
maxValue = (*it).value;
maxValueIt = it;
}
++intervalDataCount;
} else // new pixel started
{
if (intervalDataCount >= 2) // last pixel had multiple data points, consolidate them
{
// determine value pixel span and add as many points in interval to maintain certain vertical data density (this is specific to scatter plot):
double valuePixelSpan = qAbs(valueAxis->coordToPixel(minValue)-valueAxis->coordToPixel(maxValue));
int dataModulo = qMax(1, qRound(intervalDataCount/(valuePixelSpan/4.0))); // approximately every 4 value pixels one data point on average
QVector<QCPData>::const_iterator intervalIt = currentIntervalStart;
int c = 0;
while (intervalIt != it)
{
if ((c % dataModulo == 0 || intervalIt == minValueIt || intervalIt == maxValueIt) && (*intervalIt).value > valueMinRange && (*intervalIt).value < valueMaxRange)
scatterData->append((*intervalIt));
++c;
++intervalIt;
}
} else if ((*currentIntervalStart).value > valueMinRange && (*currentIntervalStart).value < valueMaxRange)
scatterData->append((*currentIntervalStart));
minValue = (*it).value;
maxValue = (*it).value;
currentIntervalStart = it;
currentIntervalStartKey = keyAxis->pixelToCoord((int)(keyAxis->coordToPixel((*it).key)+reversedRound));
if (keyEpsilonVariable)
keyEpsilon = qAbs(currentIntervalStartKey-keyAxis->pixelToCoord(keyAxis->coordToPixel(currentIntervalStartKey)+1.0*reversedFactor));
intervalDataCount = 1;
}
++it;
}
// handle last interval:
if (intervalDataCount >= 2) // last pixel had multiple data points, consolidate them
{
// determine value pixel span and add as many points in interval to maintain certain vertical data density (this is specific to scatter plot):
double valuePixelSpan = qAbs(valueAxis->coordToPixel(minValue)-valueAxis->coordToPixel(maxValue));
int dataModulo = qMax(1, qRound(intervalDataCount/(valuePixelSpan/4.0))); // approximately every 4 value pixels one data point on average
QVector<QCPData>::const_iterator intervalIt = currentIntervalStart;
int c = 0;
while (intervalIt != it)
{
if ((c % dataModulo == 0 || intervalIt == minValueIt || intervalIt == maxValueIt) && (*intervalIt).value > valueMinRange && (*intervalIt).value < valueMaxRange)
scatterData->append((*intervalIt));
++c;
++intervalIt;
}
} else if ((*currentIntervalStart).value > valueMinRange && (*currentIntervalStart).value < valueMaxRange)
scatterData->append(*currentIntervalStart);
}
} else // don't use adaptive sampling algorithm, transfer points one-to-one from the map into the output parameters
{
QVector<QCPData> *dataVector = 0;
if (lineData)
{
dataVector = lineData;
}
else if (scatterData)
dataVector = scatterData;
if (dataVector)
{
QVector<QCPData>::const_iterator it = lower;
QVector<QCPData>::const_iterator upperEnd = upper+1;
dataVector->reserve(dataCount+2); // +2 for possible fill end points
while (it != upperEnd)
{
dataVector->append(*it);
++it;
}
}
if (lineData && scatterData)
*scatterData = *dataVector;
}
}
QVector<QCPData>::const_iterator __lowerBoundDico_vect(QVector<QCPData>* vector,double key)
{
int DX=vector->size()/2;
int pos=DX;
// double test=(*vector)[vector->length()-1].key;
if(key>((*vector)[vector->length()-1].key))
return vector->constEnd();
if(key<((*vector)[0].key))
return vector->constBegin();
while (DX>1)
{
DX=DX/2;
if((*vector)[pos].key > key)
{
pos-=DX;
}
else
{
pos+=DX;
}
}
if((*vector)[pos].key >= key)
return vector->constBegin()+pos;
return vector->constBegin()+pos+1;
}
QVector<QCPData>::const_iterator __upperBoundDico_vect(QVector<QCPData>* vector,double key)
{
int DX=vector->size()/2;
int pos=DX;
if(key>((*vector)[vector->length()-1].key))
return vector->constEnd();
if(key<((*vector)[0].key))
return vector->constBegin();
while (DX>1)
{
DX=DX/2;
if((*vector)[pos].key > key)
{
pos-=DX;
}
else
{
pos+=DX;
}
}
return vector->constBegin()+pos+1;
}
void QCPGraphVect::getVisibleDataBounds(QVector<QCPData>::const_iterator &lower, QVector<QCPData>::const_iterator &upper) const
{
if (!mKeyAxis) { qDebug() << Q_FUNC_INFO << "invalid key axis"; return; }
if (mData->isEmpty())
{
lower = mData->constEnd();
upper = mData->constEnd();
return;
}
QVector<QCPData>::const_iterator lbound = __lowerBoundDico_vect(mData,mKeyAxis.data()->range().lower);
QVector<QCPData>::const_iterator ubound = __upperBoundDico_vect(mData,mKeyAxis.data()->range().upper);
bool lowoutlier = lbound != mData->constBegin(); // indicates whether there exist points below axis range
bool highoutlier = ubound != mData->constEnd(); // indicates whether there exist points above axis range
lower = (lowoutlier ? lbound-1 : lbound); // data point range that will be actually drawn
upper = (highoutlier ? ubound : ubound-1); // data point range that will be actually drawn
}
int QCPGraphVect::countDataInBounds(const QVector<QCPData>::const_iterator &lower, const QVector<QCPData>::const_iterator &upper, int maxCount) const
{
if (upper == mData->constEnd() && lower == mData->constEnd())
return 0;
QVector<QCPData>::const_iterator it = lower;
int count = 1;
while (it != upper && count < maxCount)
{
++it;
++count;
}
return count;
}