This QPainter subclass is used to provide some extended functionality e.g. for tweaking position
consistency between antialiased and non-antialiased painting. Further it provides workarounds
for QPainter quirks.
\warning This class intentionally hides non-virtual functions of QPainter, e.g. setPen, save and
restore. So while it is possible to pass a QCPPainter instance to a function that expects a
QPainter pointer, some of the workarounds and tweaks will be unavailable to the function (because
it will call the base class implementations of the functions actually hidden by QCPPainter).
*/
/*!
Creates a new QCPPainter instance and sets default values
*/
QCPPainter::QCPPainter() :
QPainter(),
mModes(pmDefault),
mIsAntialiasing(false)
{
// don't setRenderHint(QPainter::NonCosmeticDefautPen) here, because painter isn't active yet and
// a call to begin() will follow
}
/*!
Creates a new QCPPainter instance on the specified paint \a device and sets default values. Just
like the analogous QPainter constructor, begins painting on \a device immediately.
Like \ref begin, this method sets QPainter::NonCosmeticDefaultPen in Qt versions before Qt5.
*/
QCPPainter::QCPPainter(QPaintDevice *device) :
QPainter(device),
mModes(pmDefault),
mIsAntialiasing(false)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) // before Qt5, default pens used to be cosmetic if NonCosmeticDefaultPen flag isn't set. So we set it to get consistency across Qt versions.
if (isActive())
setRenderHint(QPainter::NonCosmeticDefaultPen);
#endif
}
QCPPainter::~QCPPainter()
{
}
/*!
Sets the pen of the painter and applies certain fixes to it, depending on the mode of this
QCPPainter.
\note this function hides the non-virtual base class implementation.
*/
void QCPPainter::setPen(const QPen &pen)
{
QPainter::setPen(pen);
if (mModes.testFlag(pmNonCosmetic))
makeNonCosmetic();
}
/*! \overload
Sets the pen (by color) of the painter and applies certain fixes to it, depending on the mode of
this QCPPainter.
\note this function hides the non-virtual base class implementation.
*/
void QCPPainter::setPen(const QColor &color)
{
QPainter::setPen(color);
if (mModes.testFlag(pmNonCosmetic))
makeNonCosmetic();
}
/*! \overload
Sets the pen (by style) of the painter and applies certain fixes to it, depending on the mode of
this QCPPainter.
\note this function hides the non-virtual base class implementation.
*/
void QCPPainter::setPen(Qt::PenStyle penStyle)
{
QPainter::setPen(penStyle);
if (mModes.testFlag(pmNonCosmetic))
makeNonCosmetic();
}
/*! \overload
Works around a Qt bug introduced with Qt 4.8 which makes drawing QLineF unpredictable when
antialiasing is disabled. Thus when antialiasing is disabled, it rounds the \a line to
integer coordinates and then passes it to the original drawLine.
\note this function hides the non-virtual base class implementation.
*/
void QCPPainter::drawLine(const QLineF &line)
{
if (mIsAntialiasing || mModes.testFlag(pmVectorized))
QPainter::drawLine(line);
else
QPainter::drawLine(line.toLine());
}
/*!
Sets whether painting uses antialiasing or not. Use this method instead of using setRenderHint
with QPainter::Antialiasing directly, as it allows QCPPainter to regain pixel exactness between
antialiased and non-antialiased painting (Since Qt < 5.0 uses slightly different coordinate systems for
AA/Non-AA painting).
*/
void QCPPainter::setAntialiasing(bool enabled)
{
setRenderHint(QPainter::Antialiasing, enabled);
if (mIsAntialiasing != enabled)
{
mIsAntialiasing = enabled;
if (!mModes.testFlag(pmVectorized)) // antialiasing half-pixel shift only needed for rasterized outputs
{
if (mIsAntialiasing)
translate(0.5, 0.5);
else
translate(-0.5, -0.5);
}
}
}
/*!
Sets the mode of the painter. This controls whether the painter shall adjust its
fixes/workarounds optimized for certain output devices.
Sets the QPainter::NonCosmeticDefaultPen in Qt versions before Qt5 after beginning painting on \a
device. This is necessary to get cosmetic pen consistency across Qt versions, because since Qt5,
all pens are non-cosmetic by default, and in Qt4 this render hint must be set to get that
behaviour.
The Constructor \ref QCPPainter(QPaintDevice *device) which directly starts painting also sets
the render hint as appropriate.
\note this function hides the non-virtual base class implementation.
*/
bool QCPPainter::begin(QPaintDevice *device)
{
bool result = QPainter::begin(device);
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) // before Qt5, default pens used to be cosmetic if NonCosmeticDefaultPen flag isn't set. So we set it to get consistency across Qt versions.
if (result)
setRenderHint(QPainter::NonCosmeticDefaultPen);
#endif
return result;
}
/*! \overload
Sets the mode of the painter. This controls whether the painter shall adjust its
fixes/workarounds optimized for certain output devices.
QCPLayerable(parentPlot), // parenthood is changed as soon as layout element gets inserted into a layout (except for top level layout)
mParentLayout(0),
mMinimumSize(),
mMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX),
mRect(0, 0, 0, 0),
mOuterRect(0, 0, 0, 0),
mMargins(0, 0, 0, 0),
mMinimumMargins(0, 0, 0, 0),
mAutoMargins(QCP::msAll)
{
}
QCPLayoutElement::~QCPLayoutElement()
{
setMarginGroup(QCP::msAll, 0); // unregister at margin groups, if there are any
// unregister at layout:
if (qobject_cast<QCPLayout*>(mParentLayout)) // the qobject_cast is just a safeguard in case the layout forgets to call clear() in its dtor and this dtor is called by QObject dtor
mParentLayout->take(this);
}
/*!
Sets the outer rect of this layout element. If the layout element is inside a layout, the layout
sets the position and size of this layout element using this function.
Calling this function externally has no effect, since the layout will overwrite any changes to
the outer rect upon the next replot.
The layout element will adapt its inner \ref rect by applying the margins inward to the outer rect.
if (mAutoMargins.testFlag(side)) // this side's margin shall be calculated automatically
{
if (mMarginGroups.contains(side))
QCP::setMarginValue(newMargins, side, mMarginGroups[side]->commonMargin(side)); // this side is part of a margin group, so get the margin value from that group
else
QCP::setMarginValue(newMargins, side, calculateAutoMargin(side)); // this side is not part of a group, so calculate the value directly
// apply minimum margin restrictions:
if (QCP::getMarginValue(newMargins, side) < QCP::getMarginValue(mMinimumMargins, side))
// if provided total size is forced smaller than total minimum size, ignore minimum sizes (squeeze sections):
int minSizeSum = 0;
for (int i=0; i<sectionCount; ++i)
minSizeSum += minSizes.at(i);
if (totalSize < minSizeSum)
{
// new stretch factors are minimum sizes and minimum sizes are set to zero:
for (int i=0; i<sectionCount; ++i)
{
stretchFactors[i] = minSizes.at(i);
minSizes[i] = 0;
}
}
QList<int> minimumLockedSections;
QList<int> unfinishedSections;
for (int i=0; i<sectionCount; ++i)
unfinishedSections.append(i);
double freeSize = totalSize;
int outerIterations = 0;
while (!unfinishedSections.isEmpty() && outerIterations < sectionCount*2) // the iteration check ist just a failsafe in case something really strange happens
{
++outerIterations;
int innerIterations = 0;
while (!unfinishedSections.isEmpty() && innerIterations < sectionCount*2) // the iteration check ist just a failsafe in case something really strange happens
if (!QCPRange::validRange(newRange)) // likely due to range being zero (plottable has only constant data in this axis dimension), shift current range to at least center the plottable
{
double center = (newRange.lower+newRange.upper)*0.5; // upper and lower should be equal anyway, but just to make sure, incase validRange returned false for other reason
for (int i=mLowestVisibleTick; i<=mHighestVisibleTick; ++i)
{
#if QT_VERSION < QT_VERSION_CHECK(4, 7, 0) // use fromMSecsSinceEpoch function if available, to gain sub-second accuracy on tick labels (e.g. for format "hh:mm:ss:zzz")
if (mAutoTicks) // ticks generated automatically, but not ticklabels, so emit ticksRequest here for labels
{
emit ticksRequest();
}
// make sure provided tick label vector has correct (minimal) length:
if (mTickVectorLabels.size() < mTickVector.size())
mTickVectorLabels.resize(mTickVector.size());
}
}
/*! \internal
If \ref setAutoTicks is set to true, this function is called by \ref setupTickVectors to
generate reasonable tick positions (and subtick count). The algorithm tries to create
approximately <tt>mAutoTickCount</tt> ticks (set via \ref setAutoTickCount).
If the scale is logarithmic, \ref setAutoTickCount is ignored, and one tick is generated at every
power of the current logarithm base, set via \ref setScaleLogBase.
*/
void QCPAxis::generateAutoTicks()
{
if (mScaleType == stLinear)
{
if (mAutoTickStep)
{
// Generate tick positions according to linear scaling:
mTickStep = mRange.size()/(double)(mAutoTickCount+1e-10); // mAutoTickCount ticks on average, the small addition is to prevent jitter on exact integers
double magnitudeFactor = qPow(10.0, qFloor(qLn(mTickStep)/qLn(10.0))); // get magnitude factor e.g. 0.01, 1, 10, 1000 etc.
baseLine = QLineF(baseLine.p2(), baseLine.p1()); // won't make a difference for line itself, but for line endings later
painter->drawLine(baseLine);
// draw ticks:
if (!tickPositions.isEmpty())
{
painter->setPen(tickPen);
int tickDir = (type == QCPAxis::atBottom || type == QCPAxis::atRight) ? -1 : 1; // direction of ticks ("inward" is right for left axis and left for right axis)
if (eLast > ePos) // only if also to right of 'e' is a digit/+/- interpret it as beautifiable power
useBeautifulPowers = true;
}
}
// calculate text bounding rects and do string preparation for beautiful decimal powers:
result.baseFont = font;
if (result.baseFont.pointSizeF() > 0) // might return -1 if specified with setPixelSize, in that case we can't do correction in next line
result.baseFont.setPointSizeF(result.baseFont.pointSizeF()+0.05); // QFontMetrics.boundingRect has a bug for exact point sizes that make the results oscillate due to internal rounding
if (useBeautifulPowers)
{
// split text into parts of number/symbol that will be drawn normally and part that will be drawn as exponent:
result.basePart = text.left(ePos);
// in log scaling, we want to turn "1*10^n" into "10^n", else add multiplication sign and decimal base:
if (abbreviateDecimalPowers && result.basePart == QLatin1String("1"))
result.totalBounds = result.baseBounds.adjusted(0, 0, result.expBounds.width()+2, 0); // +2 consists of the 1 pixel spacing between base and exponent (see drawTickLabel) and an extra pixel to include AA
result.totalBounds.moveTopLeft(QPoint(0, 0)); // want bounding box aligned top left at origin, independent of how it was created, to make further processing simpler
// calculate possibly different bounding rect after rotation:
if ((type == QCPAxis::atLeft && tickLabelSide == QCPAxis::lsOutside) || (type == QCPAxis::atRight && tickLabelSide == QCPAxis::lsInside)) // Anchor at right side of tick label
{
if (doRotation)
{
if (tickLabelRotation > 0)
{
x = -qCos(radians)*labelData.totalBounds.width();
y = flip ? -labelData.totalBounds.width()/2.0 : -qSin(radians)*labelData.totalBounds.width()-qCos(radians)*labelData.totalBounds.height()/2.0;
} else
{
x = -qCos(-radians)*labelData.totalBounds.width()-qSin(-radians)*labelData.totalBounds.height();
y = flip ? +labelData.totalBounds.width()/2.0 : +qSin(-radians)*labelData.totalBounds.width()-qCos(-radians)*labelData.totalBounds.height()/2.0;
}
} else
{
x = -labelData.totalBounds.width();
y = -labelData.totalBounds.height()/2.0;
}
} else if ((type == QCPAxis::atRight && tickLabelSide == QCPAxis::lsOutside) || (type == QCPAxis::atLeft && tickLabelSide == QCPAxis::lsInside)) // Anchor at left side of tick label
{
if (doRotation)
{
if (tickLabelRotation > 0)
{
x = +qSin(radians)*labelData.totalBounds.height();
y = flip ? -labelData.totalBounds.width()/2.0 : -qCos(radians)*labelData.totalBounds.height()/2.0;
} else
{
x = 0;
y = flip ? +labelData.totalBounds.width()/2.0 : -qCos(-radians)*labelData.totalBounds.height()/2.0;
}
} else
{
x = 0;
y = -labelData.totalBounds.height()/2.0;
}
} else if ((type == QCPAxis::atTop && tickLabelSide == QCPAxis::lsOutside) || (type == QCPAxis::atBottom && tickLabelSide == QCPAxis::lsInside)) // Anchor at bottom side of tick label
{
if (doRotation)
{
if (tickLabelRotation > 0)
{
x = -qCos(radians)*labelData.totalBounds.width()+qSin(radians)*labelData.totalBounds.height()/2.0;
y = -qSin(radians)*labelData.totalBounds.width()-qCos(radians)*labelData.totalBounds.height();
} else
{
x = -qSin(-radians)*labelData.totalBounds.height()/2.0;
y = -qCos(-radians)*labelData.totalBounds.height();
}
} else
{
x = -labelData.totalBounds.width()/2.0;
y = -labelData.totalBounds.height();
}
} else if ((type == QCPAxis::atBottom && tickLabelSide == QCPAxis::lsOutside) || (type == QCPAxis::atTop && tickLabelSide == QCPAxis::lsInside)) // Anchor at top side of tick label
{
if (doRotation)
{
if (tickLabelRotation > 0)
{
x = +qSin(radians)*labelData.totalBounds.height()/2.0;
y = 0;
} else
{
x = -qCos(-radians)*labelData.totalBounds.width()-qSin(-radians)*labelData.totalBounds.height()/2.0;
y = +qSin(-radians)*labelData.totalBounds.width();
}
} else
{
x = -labelData.totalBounds.width()/2.0;
y = 0;
}
}
return QPointF(x, y);
}
/*! \internal
Simulates the steps done by \ref placeTickLabel by calculating bounding boxes of the text label
to be drawn, depending on number format etc. Since only the largest tick label is wanted for the
margin calculation, the passed \a tickLabelsSize is only expanded, if it's currently set to a
\brief The abstract base class for all data representing objects in a plot.
It defines a very basic interface like name, pen, brush, visibility etc. Since this class is
abstract, it can't be instantiated. Use one of the subclasses or create a subclass yourself to
create new ways of displaying data (see "Creating own plottables" below).
All further specifics are in the subclasses, for example:
\li A normal graph with possibly a line, scatter points and error bars: \ref QCPGraph
(typically created with \ref QCustomPlot::addGraph)
\li A parametric curve: \ref QCPCurve
\li A bar chart: \ref QCPBars
\li A statistical box plot: \ref QCPStatisticalBox
\li A color encoded two-dimensional map: \ref QCPColorMap
\li An OHLC/Candlestick chart: \ref QCPFinancial
\section plottables-subclassing Creating own plottables
To create an own plottable, you implement a subclass of QCPAbstractPlottable. These are the pure
virtual functions, you must implement:
\li \ref clearData
\li \ref selectTest
\li \ref draw
\li \ref drawLegendIcon
\li \ref getKeyRange
\li \ref getValueRange
See the documentation of those functions for what they need to do.
For drawing your plot, you can use the \ref coordsToPixels functions to translate a point in plot
coordinates to pixel coordinates. This function is quite convenient, because it takes the
orientation of the key and value axes into account for you (x and y are swapped when the key axis
is vertical and the value axis horizontal). If you are worried about performance (i.e. you need
to translate many points in a loop like QCPGraph), you can directly use \ref
QCPAxis::coordToPixel. However, you must then take care about the orientation of the axis
yourself.
Here are some important members you inherit from QCPAbstractPlottable:
<table>
<tr>
<td>QCustomPlot *\b mParentPlot</td>
<td>A pointer to the parent QCustomPlot instance. The parent plot is inferred from the axes that are passed in the constructor.</td>
</tr><tr>
<td>QString \b mName</td>
<td>The name of the plottable.</td>
</tr><tr>
<td>QPen \b mPen</td>
<td>The generic pen of the plottable. You should use this pen for the most prominent data representing lines in the plottable (e.g QCPGraph uses this pen for its graph lines and scatters)</td>
</tr><tr>
<td>QPen \b mSelectedPen</td>
<td>The generic pen that should be used when the plottable is selected (hint: \ref mainPen gives you the right pen, depending on selection state).</td>
</tr><tr>
<td>QBrush \b mBrush</td>
<td>The generic brush of the plottable. You should use this brush for the most prominent fillable structures in the plottable (e.g. QCPGraph uses this brush to control filling under the graph)</td>
</tr><tr>
<td>QBrush \b mSelectedBrush</td>
<td>The generic brush that should be used when the plottable is selected (hint: \ref mainBrush gives you the right brush, depending on selection state).</td>
<td>The key and value axes this plottable is attached to. Call their QCPAxis::coordToPixel functions to translate coordinates to pixels in either the key or value dimension.
Make sure to check whether the pointer is null before using it. If one of the axes is null, don't draw the plottable.</td>
</tr><tr>
<td>bool \b mSelected</td>
<td>indicates whether the plottable is selected or not.</td>
</tr>
</table>
*/
/* start of documentation of pure virtual functions */
if (!QCPRange::validRange(newRange)) // likely due to range being zero (plottable has only constant data in this axis dimension), shift current range to at least center the plottable
{
double center = (newRange.lower+newRange.upper)*0.5; // upper and lower should be equal anyway, but just to make sure, incase validRange returned false for other reason
if (!QCPRange::validRange(newRange)) // likely due to range being zero (plottable has only constant data in this axis dimension), shift current range to at least center the plottable
{
double center = (newRange.lower+newRange.upper)*0.5; // upper and lower should be equal anyway, but just to make sure, incase validRange returned false for other reason
\brief The abstract base class for all items in a plot.
In QCustomPlot, items are supplemental graphical elements that are neither plottables
(QCPAbstractPlottable) nor axes (QCPAxis). While plottables are always tied to two axes and thus
plot coordinates, items can also be placed in absolute coordinates independent of any axes. Each
specific item has at least one QCPItemPosition member which controls the positioning. Some items
are defined by more than one coordinate and thus have two or more QCPItemPosition members (For
example, QCPItemRect has \a topLeft and \a bottomRight).
This abstract base class defines a very basic interface like visibility and clipping. Since this
class is abstract, it can't be instantiated. Use one of the subclasses or create a subclass
yourself to create new items.
The built-in items are:
<table>
<tr><td>QCPItemLine</td><td>A line defined by a start and an end point. May have different ending styles on each side (e.g. arrows).</td></tr>
<tr><td>QCPItemStraightLine</td><td>A straight line defined by a start and a direction point. Unlike QCPItemLine, the straight line is infinitely long and has no endings.</td></tr>
<tr><td>QCPItemCurve</td><td>A curve defined by start, end and two intermediate control points. May have different ending styles on each side (e.g. arrows).</td></tr>
double resultDistance = mSelectionTolerance; // only regard clicks with distances smaller than mSelectionTolerance as selections, so initialize with that value
if (onlySelectable && !plottable->selectable()) // we could have also passed onlySelectable to the selectTest function, but checking here is faster, because we have access to QCPabstractPlottable::selectable
continue;
if ((plottable->keyAxis()->axisRect()->rect() & plottable->valueAxis()->axisRect()->rect()).contains(pos.toPoint())) // only consider clicks inside the rect that is spanned by the plottable's key/value axes
double resultDistance = mSelectionTolerance; // only regard clicks with distances smaller than mSelectionTolerance as selections, so initialize with that value
foreach (QCPAbstractItem *item, mItems)
{
if (onlySelectable && !item->selectable()) // we could have also passed onlySelectable to the selectTest function, but checking here is faster, because we have access to QCPAbstractItem::selectable
continue;
if (!item->clipToAxisRect() || item->clipRect().contains(pos.toPoint())) // only consider clicks inside axis cliprect of the item if actually clipped to it
else if (QCPPlotTitle *pt = qobject_cast<QCPPlotTitle*>(clickedLayerable))
emit titleDoubleClick(event, pt);
// call double click event of affected layout element:
if (QCPLayoutElement *el = layoutElementAt(event->pos()))
el->mouseDoubleClickEvent(event);
// call release event of affected layout element (as in mouseReleaseEvent, since the mouseDoubleClick replaces the second release event in double click case):
if (mMouseEventElement)
{
mMouseEventElement->mouseReleaseEvent(event);
mMouseEventElement = 0;
}
//QWidget::mouseDoubleClickEvent(event); don't call base class implementation because it would just cause a mousePress/ReleaseEvent, which we don't want.
}
/*! \internal
Event handler for when a mouse button is pressed. Emits the mousePress signal. Then determines
the affected layout element and forwards the event to it.
QCPLayerable *clickedLayerable = layerableAt(event->pos(), false, &details); // for these signals, selectability is ignored, that's why we call this again with onlySelectable set to false
if (QCPAbstractPlottable *ap = qobject_cast<QCPAbstractPlottable*>(clickedLayerable))
emit plottableClick(ap, event);
else if (QCPAxis *ax = qobject_cast<QCPAxis*>(clickedLayerable))
QPixmap QCustomPlot::toPixmap(int width, int height, double scale)
{
// this method is somewhat similar to toPainter. Change something here, and a change in toPainter might be necessary, too.
int newWidth, newHeight;
if (width == 0 || height == 0)
{
newWidth = this->width();
newHeight = this->height();
} else
{
newWidth = width;
newHeight = height;
}
int scaledWidth = qRound(scale*newWidth);
int scaledHeight = qRound(scale*newHeight);
QPixmap result(scaledWidth, scaledHeight);
result.fill(mBackgroundBrush.style() == Qt::SolidPattern ? mBackgroundBrush.color() : Qt::transparent); // if using non-solid pattern, make transparent now and draw brush pattern later
QCPPainter painter;
painter.begin(&result);
if (painter.isActive())
{
QRect oldViewport = viewport();
setViewport(QRect(0, 0, newWidth, newHeight));
painter.setMode(QCPPainter::pmNoCaching);
if (!qFuzzyCompare(scale, 1.0))
{
if (scale > 1.0) // for scale < 1 we always want cosmetic pens where possible, because else lines might disappear for very small scales
painter.setMode(QCPPainter::pmNonCosmetic);
painter.scale(scale, scale);
}
if (mBackgroundBrush.style() != Qt::SolidPattern && mBackgroundBrush.style() != Qt::NoBrush) // solid fills were done a few lines above with QPixmap::fill
painter.fillRect(mViewport, mBackgroundBrush);
draw(&painter);
setViewport(oldViewport);
painter.end();
} else // might happen if pixmap has width or height zero
{
qDebug() << Q_FUNC_INFO << "Couldn't activate painter on pixmap";
return QPixmap();
}
return result;
}
/*!
Renders the plot using the passed \a painter.
The plot is sized to \a width and \a height in pixels. If the \a painter's scale is not 1.0, the resulting plot will
appear scaled accordingly.
\note If you are restricted to using a QPainter (instead of QCPPainter), create a temporary QPicture and open a QCPPainter
on it. Then call \ref toPainter with this QCPPainter. After ending the paint operation on the picture, draw it with
the QPainter. This will reproduce the painter actions the QCPPainter took, with a QPainter.
\see toPixmap
*/
void QCustomPlot::toPainter(QCPPainter *painter, int width, int height)
{
// this method is somewhat similar to toPixmap. Change something here, and a change in toPixmap might be necessary, too.
int newWidth, newHeight;
if (width == 0 || height == 0)
{
newWidth = this->width();
newHeight = this->height();
} else
{
newWidth = width;
newHeight = height;
}
if (painter->isActive())
{
QRect oldViewport = viewport();
setViewport(QRect(0, 0, newWidth, newHeight));
painter->setMode(QCPPainter::pmNoCaching);
if (mBackgroundBrush.style() != Qt::NoBrush) // unlike in toPixmap, we can't do QPixmap::fill for Qt::SolidPattern brush style, so we also draw solid fills with fillRect here
painter->fillRect(mViewport, mBackgroundBrush);
draw(painter);
setViewport(oldViewport);
} else
qDebug() << Q_FUNC_INFO << "Passed painter is not active";
if (qobject_cast<QCustomPlot*>(parentPlot())) // make sure this isn't called from QObject dtor when QCustomPlot is already destructed (happens when the axis rect is not in any layout and thus QObject-child of QCustomPlot)
bool isFirstVisible = !axesList.first()->visible(); // if the first axis is visible, the second axis (which is where the loop starts) isn't the first visible axis, so initialize with false
for (int i=1; i<axesList.size(); ++i)
{
int offset = axesList.at(i-1)->offset() + axesList.at(i-1)->calculateMargin();
if (axesList.at(i)->visible()) // only add inner tick length to offset if this axis is visible and it's not the first visible one (might happen if true first axis is invisible)
{
if (!isFirstVisible)
offset += axesList.at(i)->tickLengthIn();
isFirstVisible = false;
}
axesList.at(i)->setOffset(offset);
}
}
/* inherits documentation from base class */
int QCPAxisRect::calculateAutoMargin(QCP::MarginSide side)
{
if (!mAutoMargins.testFlag(side))
qDebug() << Q_FUNC_INFO << "Called with side that isn't specified as auto margin";
mDragStart = event->pos(); // need this even when not LeftButton is pressed, to determine in releaseEvent whether it was a full click (no position change between press and release)
if (event->buttons() & Qt::LeftButton)
{
mDragging = true;
// initialize antialiasing backup in case we start dragging:
int textHeight = qMax(textRect.height(), iconSize.height()); // if text has smaller height than icon, center text vertically in icon height, else align tops
This signal is emitted when the selection state of this legend has changed.
\see setSelectedParts, setSelectableParts
*/
/* end of documentation of signals */
/*!
Constructs a new QCPLegend instance with \a parentPlot as the containing plot and default values.
Note that by default, QCustomPlot already contains a legend ready to be used as
QCustomPlot::legend
*/
QCPLegend::QCPLegend()
{
setRowSpacing(0);
setColumnSpacing(10);
setMargins(QMargins(2, 3, 2, 2));
setAntialiased(false);
setIconSize(32, 18);
setIconTextPadding(7);
setSelectableParts(spLegendBox | spItems);
setSelectedParts(spNone);
setBorderPen(QPen(Qt::black));
setSelectedBorderPen(QPen(Qt::blue, 2));
setIconBorderPen(Qt::NoPen);
setSelectedIconBorderPen(QPen(Qt::blue, 2));
setBrush(Qt::white);
setSelectedBrush(Qt::white);
setTextColor(Qt::black);
setSelectedTextColor(Qt::blue);
}
QCPLegend::~QCPLegend()
{
clearItems();
if (qobject_cast<QCustomPlot*>(mParentPlot)) // make sure this isn't called from QObject dtor when QCustomPlot is already destructed (happens when the legend is not in any layout and thus QObject-child of QCustomPlot)
mSelectedParts = selectedParts(); // in case item selection has changed
if (details.value<SelectablePart>() == spLegendBox && mSelectableParts.testFlag(spLegendBox))
{
SelectableParts selBefore = mSelectedParts;
setSelectedParts(additive ? mSelectedParts^spLegendBox : mSelectedParts|spLegendBox); // no need to unset spItems in !additive case, because they will be deselected by QCustomPlot (they're normal QCPLayerables with own deselectEvent)
mType(QCPAxis::atTop), // set to atTop such that setType(QCPAxis::atRight) below doesn't skip work because it thinks it's already atRight
mDataScaleType(QCPAxis::stLinear),
mBarWidth(20),
mAxisRect(new QCPColorScaleAxisRectPrivate(this))
{
setMinimumMargins(QMargins(0, 6, 0, 6)); // for default right color scale types, keep some room at bottom and top (important if no margin group is used)
setType(QCPAxis::atRight);
setDataRange(QCPRange(0, 6));
}
QCPColorScale::~QCPColorScale()
{
delete mAxisRect;
}
/* undocumented getter */
QString QCPColorScale::label() const
{
if (!mColorAxis)
{
qDebug() << Q_FUNC_INFO << "internal color axis undefined";
return QString();
}
return mColorAxis.data()->label();
}
/* undocumented getter */
bool QCPColorScale::rangeDrag() const
{
if (!mAxisRect)
{
qDebug() << Q_FUNC_INFO << "internal axis rect was deleted";
mColorAxis.data()->setRange(rangeTransfer); // transfer range of old axis to new one (necessary if axis changes from vertical to horizontal or vice versa)
mColorAxis.data()->setLabel(labelTransfer);
mColorAxis.data()->setScaleLogBase(logBaseTransfer); // scaleType is synchronized among axes in realtime via signals (connected in QCPColorScale ctor), so we only need to take care of log base here
int sign = 0; // TODO: should change this to QCPAbstractPlottable::SignDomain later (currently is protected, maybe move to QCP namespace)
if (mDataScaleType == QCPAxis::stLogarithmic)
sign = (mDataRange.upper < 0 ? -1 : 1);
for (int i=0; i<maps.size(); ++i)
{
if (!maps.at(i)->realVisibility() && onlyVisibleMaps)
continue;
QCPRange mapRange;
if (maps.at(i)->colorScale() == this)
{
bool currentFoundRange = true;
mapRange = maps.at(i)->data()->dataBounds();
if (sign == 1)
{
if (mapRange.lower <= 0 && mapRange.upper > 0)
mapRange.lower = mapRange.upper*1e-3;
else if (mapRange.lower <= 0 && mapRange.upper <= 0)
currentFoundRange = false;
} else if (sign == -1)
{
if (mapRange.upper >= 0 && mapRange.lower < 0)
mapRange.upper = mapRange.lower*1e-3;
else if (mapRange.upper >= 0 && mapRange.lower >= 0)
currentFoundRange = false;
}
if (currentFoundRange)
{
if (!haveRange)
newRange = mapRange;
else
newRange.expand(mapRange);
haveRange = true;
}
}
}
if (haveRange)
{
if (!QCPRange::validRange(newRange)) // likely due to range being zero (plottable has only constant data in this dimension), shift current range to at least center the data
{
double center = (newRange.lower+newRange.upper)*0.5; // upper and lower should be equal anyway, but just to make sure, incase validRange returned false for other reason
painter->drawLine(QLineF(rect.left(), rect.top()+rect.height()/2.0, rect.right()+5, rect.top()+rect.height()/2.0)); // +5 on x2 else last segment is missing from dashed/dotted pens
}
// draw scatter symbol:
if (!mScatterStyle.isNone())
{
applyScattersAntialiasingHint(painter);
// scale scatter pixmap if it's too large to fit in legend icon rect:
if (qIsNaN(lineData->at(i).y()) || qIsNaN(lineData->at(i).x()) || qIsInf(lineData->at(i).y())) // NaNs create a gap in the line. Also filter Infs which make drawPolyline block
{
painter->drawPolyline(lineData->constData()+segmentStart, i-segmentStart); // i, because we don't want to include the current NaN point
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 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().value < minValue)
minValue = it.value().value;
else if (it.value().value > maxValue)
maxValue = it.value().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
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
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
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 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 (!keyAxis || !valueAxis) { qDebug() << Q_FUNC_INFO << "invalid key or value axis"; return QPolygonF(); }
if (!mChannelFillGraph.data()->mKeyAxis) { qDebug() << Q_FUNC_INFO << "channel fill target key axis invalid"; return QPolygonF(); }
if (mChannelFillGraph.data()->mKeyAxis.data()->orientation() != keyAxis->orientation())
return QPolygonF(); // don't have same axis orientation, can't fill that (Note: if keyAxis fits, valueAxis will fit too, because it's always orthogonal to keyAxis)
painter->drawLine(QLineF(rect.left(), rect.top()+rect.height()/2.0, rect.right()+5, rect.top()+rect.height()/2.0)); // +5 on x2 else last segment is missing from dashed/dotted pens
}
// draw scatter symbol:
if (!mScatterStyle.isNone())
{
applyScattersAntialiasingHint(painter);
// scale scatter pixmap if it's too large to fit in legend icon rect:
int prevRegion = getRegion(prevIt.value().key, prevIt.value().value, rectLeft, rectTop, rectRight, rectBottom);
QVector<QPointF> trailingPoints; // points that must be applied after all other points (are generated only when handling first point to get virtual segment between last and first point right)
// in the situations 5->1/7/9/3 the segment may leave R and directly cross through two outer regions. In these cases we need to add an additional corner point
case 2: { result << coordsToPixels(rectLeft, rectTop); break; }
case 4: { result << coordsToPixels(rectLeft, rectTop); break; }
case 3: { result << coordsToPixels(rectLeft, rectTop) << coordsToPixels(rectLeft, rectBottom); break; }
case 7: { result << coordsToPixels(rectLeft, rectTop) << coordsToPixels(rectRight, rectTop); break; }
case 6: { result << coordsToPixels(rectLeft, rectTop) << coordsToPixels(rectLeft, rectBottom); result.append(result.last()); break; }
case 8: { result << coordsToPixels(rectLeft, rectTop) << coordsToPixels(rectRight, rectTop); result.append(result.last()); break; }
case 9: { // in this case we need another distinction of cases: segment may pass below or above rect, requiring either bottom right or top left corner points
if ((value-prevValue)/(key-prevKey)*(rectLeft-key)+value < rectBottom) // segment passes below R
{ result << coordsToPixels(rectLeft, rectTop) << coordsToPixels(rectLeft, rectBottom); result.append(result.last()); result << coordsToPixels(rectRight, rectBottom); }
else
{ result << coordsToPixels(rectLeft, rectTop) << coordsToPixels(rectRight, rectTop); result.append(result.last()); result << coordsToPixels(rectRight, rectBottom); }
break;
}
}
break;
}
case 2:
{
switch (currentRegion)
{
case 1: { result << coordsToPixels(rectLeft, rectTop); break; }
case 3: { result << coordsToPixels(rectLeft, rectBottom); break; }
case 4: { result << coordsToPixels(rectLeft, rectTop); result.append(result.last()); break; }
case 6: { result << coordsToPixels(rectLeft, rectBottom); result.append(result.last()); break; }
case 7: { result << coordsToPixels(rectLeft, rectTop); result.append(result.last()); result << coordsToPixels(rectRight, rectTop); break; }
case 9: { result << coordsToPixels(rectLeft, rectBottom); result.append(result.last()); result << coordsToPixels(rectRight, rectBottom); break; }
}
break;
}
case 3:
{
switch (currentRegion)
{
case 2: { result << coordsToPixels(rectLeft, rectBottom); break; }
case 6: { result << coordsToPixels(rectLeft, rectBottom); break; }
case 1: { result << coordsToPixels(rectLeft, rectBottom) << coordsToPixels(rectLeft, rectTop); break; }
case 9: { result << coordsToPixels(rectLeft, rectBottom) << coordsToPixels(rectRight, rectBottom); break; }
case 4: { result << coordsToPixels(rectLeft, rectBottom) << coordsToPixels(rectLeft, rectTop); result.append(result.last()); break; }
case 8: { result << coordsToPixels(rectLeft, rectBottom) << coordsToPixels(rectRight, rectBottom); result.append(result.last()); break; }
case 7: { // in this case we need another distinction of cases: segment may pass below or above rect, requiring either bottom right or top left corner points
if ((value-prevValue)/(key-prevKey)*(rectRight-key)+value < rectBottom) // segment passes below R
{ result << coordsToPixels(rectLeft, rectBottom) << coordsToPixels(rectRight, rectBottom); result.append(result.last()); result << coordsToPixels(rectRight, rectTop); }
else
{ result << coordsToPixels(rectLeft, rectBottom) << coordsToPixels(rectLeft, rectTop); result.append(result.last()); result << coordsToPixels(rectRight, rectTop); }
break;
}
}
break;
}
case 4:
{
switch (currentRegion)
{
case 1: { result << coordsToPixels(rectLeft, rectTop); break; }
case 7: { result << coordsToPixels(rectRight, rectTop); break; }
case 2: { result << coordsToPixels(rectLeft, rectTop); result.append(result.last()); break; }
case 8: { result << coordsToPixels(rectRight, rectTop); result.append(result.last()); break; }
case 3: { result << coordsToPixels(rectLeft, rectTop); result.append(result.last()); result << coordsToPixels(rectLeft, rectBottom); break; }
case 9: { result << coordsToPixels(rectRight, rectTop); result.append(result.last()); result << coordsToPixels(rectRight, rectBottom); break; }
}
break;
}
case 5:
{
switch (currentRegion)
{
case 1: { result << coordsToPixels(rectLeft, rectTop); break; }
case 7: { result << coordsToPixels(rectRight, rectTop); break; }
case 9: { result << coordsToPixels(rectRight, rectBottom); break; }
case 3: { result << coordsToPixels(rectLeft, rectBottom); break; }
}
break;
}
case 6:
{
switch (currentRegion)
{
case 3: { result << coordsToPixels(rectLeft, rectBottom); break; }
case 9: { result << coordsToPixels(rectRight, rectBottom); break; }
case 2: { result << coordsToPixels(rectLeft, rectBottom); result.append(result.last()); break; }
case 8: { result << coordsToPixels(rectRight, rectBottom); result.append(result.last()); break; }
case 1: { result << coordsToPixels(rectLeft, rectBottom); result.append(result.last()); result << coordsToPixels(rectLeft, rectTop); break; }
case 7: { result << coordsToPixels(rectRight, rectBottom); result.append(result.last()); result << coordsToPixels(rectRight, rectTop); break; }
}
break;
}
case 7:
{
switch (currentRegion)
{
case 4: { result << coordsToPixels(rectRight, rectTop); break; }
case 8: { result << coordsToPixels(rectRight, rectTop); break; }
case 1: { result << coordsToPixels(rectRight, rectTop) << coordsToPixels(rectLeft, rectTop); break; }
case 9: { result << coordsToPixels(rectRight, rectTop) << coordsToPixels(rectRight, rectBottom); break; }
case 2: { result << coordsToPixels(rectRight, rectTop) << coordsToPixels(rectLeft, rectTop); result.append(result.last()); break; }
case 6: { result << coordsToPixels(rectRight, rectTop) << coordsToPixels(rectRight, rectBottom); result.append(result.last()); break; }
case 3: { // in this case we need another distinction of cases: segment may pass below or above rect, requiring either bottom right or top left corner points
if ((value-prevValue)/(key-prevKey)*(rectRight-key)+value < rectBottom) // segment passes below R
{ result << coordsToPixels(rectRight, rectTop) << coordsToPixels(rectRight, rectBottom); result.append(result.last()); result << coordsToPixels(rectLeft, rectBottom); }
else
{ result << coordsToPixels(rectRight, rectTop) << coordsToPixels(rectLeft, rectTop); result.append(result.last()); result << coordsToPixels(rectLeft, rectBottom); }
break;
}
}
break;
}
case 8:
{
switch (currentRegion)
{
case 7: { result << coordsToPixels(rectRight, rectTop); break; }
case 9: { result << coordsToPixels(rectRight, rectBottom); break; }
case 4: { result << coordsToPixels(rectRight, rectTop); result.append(result.last()); break; }
case 6: { result << coordsToPixels(rectRight, rectBottom); result.append(result.last()); break; }
case 1: { result << coordsToPixels(rectRight, rectTop); result.append(result.last()); result << coordsToPixels(rectLeft, rectTop); break; }
case 3: { result << coordsToPixels(rectRight, rectBottom); result.append(result.last()); result << coordsToPixels(rectLeft, rectBottom); break; }
}
break;
}
case 9:
{
switch (currentRegion)
{
case 6: { result << coordsToPixels(rectRight, rectBottom); break; }
case 8: { result << coordsToPixels(rectRight, rectBottom); break; }
case 3: { result << coordsToPixels(rectRight, rectBottom) << coordsToPixels(rectLeft, rectBottom); break; }
case 7: { result << coordsToPixels(rectRight, rectBottom) << coordsToPixels(rectRight, rectTop); break; }
case 2: { result << coordsToPixels(rectRight, rectBottom) << coordsToPixels(rectLeft, rectBottom); result.append(result.last()); break; }
case 4: { result << coordsToPixels(rectRight, rectBottom) << coordsToPixels(rectRight, rectTop); result.append(result.last()); break; }
case 1: { // in this case we need another distinction of cases: segment may pass below or above rect, requiring either bottom right or top left corner points
if ((value-prevValue)/(key-prevKey)*(rectLeft-key)+value < rectBottom) // segment passes below R
{ result << coordsToPixels(rectRight, rectBottom) << coordsToPixels(rectLeft, rectBottom); result.append(result.last()); result << coordsToPixels(rectLeft, rectTop); }
else
{ result << coordsToPixels(rectRight, rectBottom) << coordsToPixels(rectRight, rectTop); result.append(result.last()); result << coordsToPixels(rectLeft, rectTop); }
break;
}
}
break;
}
}
return result;
}
/*! \internal
This function is part of the curve optimization algorithm of \ref getCurveData.
This method returns whether a segment going from \a prevRegion to \a currentRegion (see \ref
getRegion) may traverse the visible region 5. This function assumes that neither \a prevRegion
nor \a currentRegion is 5 itself.
If this method returns false, the segment for sure doesn't pass region 5. If it returns true, the
segment may or may not pass region 5 and a more fine-grained calculation must be used (\ref
getTraverse).
*/
bool QCPCurve::mayTraverse(int prevRegion, int currentRegion) const
{
switch (prevRegion)
{
case 1:
{
switch (currentRegion)
{
case 4:
case 7:
case 2:
case 3: return false;
default: return true;
}
}
case 2:
{
switch (currentRegion)
{
case 1:
case 3: return false;
default: return true;
}
}
case 3:
{
switch (currentRegion)
{
case 1:
case 2:
case 6:
case 9: return false;
default: return true;
}
}
case 4:
{
switch (currentRegion)
{
case 1:
case 7: return false;
default: return true;
}
}
case 5: return false; // should never occur
case 6:
{
switch (currentRegion)
{
case 3:
case 9: return false;
default: return true;
}
}
case 7:
{
switch (currentRegion)
{
case 1:
case 4:
case 8:
case 9: return false;
default: return true;
}
}
case 8:
{
switch (currentRegion)
{
case 7:
case 9: return false;
default: return true;
}
}
case 9:
{
switch (currentRegion)
{
case 3:
case 6:
case 8:
case 7: return false;
default: return true;
}
}
default: return true;
}
}
/*! \internal
This function is part of the curve optimization algorithm of \ref getCurveData.
This method assumes that the \ref mayTraverse test has returned true, so there is a chance the
segment defined by (\a prevKey, \a prevValue) and (\a key, \a value) goes through the visible
region 5.
The return value of this method indicates whether the segment actually traverses region 5 or not.
If the segment traverses 5, the output parameters \a crossA and \a crossB indicate the entry and
exit points of region 5. They will become the optimized points for that segment.
// one or even zero points found (shouldn't happen unless line perfectly tangent to corner), no need to draw segment
return false;
}
// possibly re-sort points so optimized point segment has same direction as original segment:
if ((key-prevKey)*(intersections.at(1).x()-intersections.at(0).x()) + (value-prevValue)*(intersections.at(1).y()-intersections.at(0).y()) < 0) // scalar product of both segments < 0 -> opposite direction
if (!mKeyAxis || !mValueAxis) { qDebug() << Q_FUNC_INFO << "invalid key or value axis"; return -1; }
if (mKeyAxis.data()->axisRect()->rect().contains(pos.toPoint()))
{
double posKey, posValue;
pixelsToCoords(pos, posKey, posValue);
if (mMapData->keyRange().contains(posKey) && mMapData->valueRange().contains(posValue))
return mParentPlot->selectionTolerance()*0.99;
}
return -1;
}
/*! \internal
Updates the internal map image buffer by going through the internal \ref QCPColorMapData and
turning the data values into color pixels with \ref QCPColorGradient::colorize.
This method is called by \ref QCPColorMap::draw if either the data has been modified or the map image
has been invalidated for a different reason (e.g. a change of the data range with \ref
setDataRange).
If the map cell count is low, the image created will be oversampled in order to avoid a
QPainter::drawImage bug which makes inner pixel boundaries jitter when stretch-drawing images
without smooth transform enabled. Accordingly, oversampling isn't performed if \ref
setInterpolate is true.
*/
void QCPColorMap::updateMapImage()
{
QCPAxis *keyAxis = mKeyAxis.data();
if (!keyAxis) return;
if (mMapData->isEmpty()) return;
const int keySize = mMapData->keySize();
const int valueSize = mMapData->valueSize();
int keyOversamplingFactor = mInterpolate ? 1 : (int)(1.0+100.0/(double)keySize); // make mMapImage have at least size 100, factor becomes 1 if size > 200 or interpolation is on
int valueOversamplingFactor = mInterpolate ? 1 : (int)(1.0+100.0/(double)valueSize); // make mMapImage have at least size 100, factor becomes 1 if size > 200 or interpolation is on
// resize mMapImage to correct dimensions including possible oversampling factors, according to key/value axes orientation:
QImage *localMapImage = &mMapImage; // this is the image on which the colorization operates. Either the final mMapImage, or if we need oversampling, mUndersampledMapImage
if (keyOversamplingFactor > 1 || valueOversamplingFactor > 1)
{
// resize undersampled map image to actual key/value cell sizes:
localMapImage = &mUndersampledMapImage; // make the colorization run on the undersampled image
} else if (!mUndersampledMapImage.isNull())
mUndersampledMapImage = QImage(); // don't need oversampling mechanism anymore (map size has changed) but mUndersampledMapImage still has nonzero size, free it
const double *rawData = mMapData->mData;
if (keyAxis->orientation() == Qt::Horizontal)
{
const int lineCount = valueSize;
const int rowCount = keySize;
for (int line=0; line<lineCount; ++line)
{
QRgb* pixels = reinterpret_cast<QRgb*>(localMapImage->scanLine(lineCount-1-line)); // invert scanline index because QImage counts scanlines from top, but our vertical index counts from bottom (mathematical coordinate system)
QRgb* pixels = reinterpret_cast<QRgb*>(localMapImage->scanLine(lineCount-1-line)); // invert scanline index because QImage counts scanlines from top, but our vertical index counts from bottom (mathematical coordinate system)