/*------------------------------------------------------------------------------ -- This file is a part of the QLop Software -- Copyright (C) 2015, Plasma Physics Laboratory - CNRS -- -- This program is free software; you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation; either version 2 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program; if not, write to the Free Software -- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -------------------------------------------------------------------------------*/ /*-- Author : Alexis Jeandet -- Mail : alexis.jeandet@member.fsf.org ----------------------------------------------------------------------------*/ /* * This code is mainly a copy of QCustomPlot's QCPGraph class */ #include "qcustomplotvect.h" #include #include 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(); } QCPGraphVect::~QCPGraphVect() { delete mData; } void QCPGraphVect::setData(QVector *data) { if(data!=mData) { delete this->mData; this->mData = data; } } QVector *QCPGraphVect::getVisibleData() { QVector* data = new QVector(); bool mAdaptiveSampling_save = mAdaptiveSampling; mAdaptiveSampling = false; getPreparedData(data,NULL); mAdaptiveSampling = mAdaptiveSampling_save; return 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 *lineData = new QVector; QVector *scatterData = 0; if (!mScatterStyle.isNone()) scatterData = new QVector; // 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 *lineData, QVector *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 *linePixelData, QVector *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 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; icoordToPixel(lineData.at(i).value)); (*linePixelData)[i].setY(keyAxis->coordToPixel(lineData.at(i).key)); } } else // key axis is horizontal { for (int i=0; icoordToPixel(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::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::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::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::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::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::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 *lineData, QVector *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::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::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::const_iterator it = lower; QVector::const_iterator upperEnd = upper+1; double minValue = (*it).value; double maxValue = (*it).value; QVector::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::const_iterator it = lower; QVector::const_iterator upperEnd = upper+1; double minValue = (*it).value; double maxValue = (*it).value; QVector::const_iterator minValueIt = it; QVector::const_iterator maxValueIt = it; QVector::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::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::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 *dataVector = 0; if (lineData) { dataVector = lineData; } else if (scatterData) dataVector = scatterData; if (dataVector) { QVector::const_iterator it = lower; QVector::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::const_iterator __lowerBoundDico_vect(QVector* 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::const_iterator __upperBoundDico_vect(QVector* 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::const_iterator &lower, QVector::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::const_iterator lbound = __lowerBoundDico_vect(mData,mKeyAxis.data()->range().lower); QVector::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::const_iterator &lower, const QVector::const_iterator &upper, int maxCount) const { if (upper == mData->constEnd() && lower == mData->constEnd()) return 0; QVector::const_iterator it = lower; int count = 1; while (it != upper && count < maxCount) { ++it; ++count; } return count; }