Revision control

Copy as Markdown

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <private/chartaxiselement_p.h>
#include <private/qabstractaxis_p.h>
#include <private/chartpresenter_p.h>
#include <private/abstractchartlayout_p.h>
#include <QtCharts/QCategoryAxis>
#include <QtCharts/QColorAxis>
#include <QtCore/QtMath>
#include <QtCore/QDateTime>
#include <QtCore/QRegularExpression>
#include <QtGui/QTextDocument>
#include <QPainter>
#include <cmath>
QT_BEGIN_NAMESPACE
static const QRegularExpression &labelFormatMatcher()
{
static const QRegularExpression re(
QLatin1String("%[\\-\\+#\\s\\d\\.\\'lhjztL]*([dicuoxfegXFEG])"));
return re;
}
static const QRegularExpression &labelFormatMatcherLocalized()
{
static const QRegularExpression re(QLatin1String("^([^%]*)%\\.(\\d+)([defgiEG])(.*)$"));
return re;
}
ChartAxisElement::ChartAxisElement(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis)
: ChartElement(item),
m_axis(axis),
m_animation(0),
m_grid(new QGraphicsItemGroup(item)),
m_arrow(new QGraphicsItemGroup(item)),
m_minorGrid(new QGraphicsItemGroup(item)),
m_minorArrow(new QGraphicsItemGroup(item)),
m_shades(new QGraphicsItemGroup(item)),
m_labels(new QGraphicsItemGroup(item)),
m_title(new QGraphicsTextItem(item)),
m_colorScale(nullptr),
m_intervalAxis(intervalAxis)
{
//initial initialization
m_arrow->setHandlesChildEvents(false);
m_arrow->setZValue(ChartPresenter::AxisZValue);
m_minorArrow->setHandlesChildEvents(false);
m_minorArrow->setZValue(ChartPresenter::AxisZValue);
m_labels->setZValue(ChartPresenter::AxisZValue);
m_shades->setZValue(ChartPresenter::ShadesZValue);
m_grid->setZValue(ChartPresenter::GridZValue);
m_minorGrid->setZValue(ChartPresenter::GridZValue);
m_title->setZValue(ChartPresenter::GridZValue);
m_title->document()->setDocumentMargin(ChartPresenter::textMargin());
if (m_axis->type() == QAbstractAxis::AxisTypeColor) {
m_colorScale = std::make_unique<QGraphicsPixmapItem>(new QGraphicsPixmapItem(item));
m_colorScale->setZValue(ChartPresenter::GridZValue);
m_colorScale->setVisible(false);
}
handleVisibleChanged(axis->isVisible());
connectSlots();
setFlag(QGraphicsItem::ItemHasNoContents, true);
}
ChartAxisElement::~ChartAxisElement()
{
}
void ChartAxisElement::connectSlots()
{
QObject::connect(axis(), SIGNAL(visibleChanged(bool)), this, SLOT(handleVisibleChanged(bool)));
QObject::connect(axis(), SIGNAL(lineVisibleChanged(bool)), this, SLOT(handleArrowVisibleChanged(bool)));
QObject::connect(axis(), SIGNAL(gridVisibleChanged(bool)), this, SLOT(handleGridVisibleChanged(bool)));
QObject::connect(axis(), SIGNAL(labelsVisibleChanged(bool)), this, SLOT(handleLabelsVisibleChanged(bool)));
QObject::connect(axis(), SIGNAL(shadesVisibleChanged(bool)), this, SLOT(handleShadesVisibleChanged(bool)));
QObject::connect(axis(), SIGNAL(labelsAngleChanged(int)), this, SLOT(handleLabelsAngleChanged(int)));
QObject::connect(axis(), SIGNAL(linePenChanged(QPen)),
this, SLOT(handleArrowPenChanged(QPen)));
QObject::connect(axis(), SIGNAL(labelsBrushChanged(QBrush)),
this, SLOT(handleLabelsBrushChanged(QBrush)));
QObject::connect(axis(), SIGNAL(labelsFontChanged(QFont)),
this, SLOT(handleLabelsFontChanged(QFont)));
QObject::connect(axis(), SIGNAL(gridLinePenChanged(QPen)),
this, SLOT(handleGridPenChanged(QPen)));
QObject::connect(axis(), SIGNAL(shadesPenChanged(QPen)),
this, SLOT(handleShadesPenChanged(QPen)));
QObject::connect(axis(), SIGNAL(shadesBrushChanged(QBrush)),
this, SLOT(handleShadesBrushChanged(QBrush)));
QObject::connect(axis(), SIGNAL(titleTextChanged(QString)),
this, SLOT(handleTitleTextChanged(QString)));
QObject::connect(axis(), SIGNAL(titleFontChanged(QFont)),
this, SLOT(handleTitleFontChanged(QFont)));
QObject::connect(axis(), SIGNAL(titleBrushChanged(QBrush)),
this, SLOT(handleTitleBrushChanged(QBrush)));
QObject::connect(axis(), SIGNAL(titleVisibleChanged(bool)), this, SLOT(handleTitleVisibleChanged(bool)));
QObject::connect(axis()->d_ptr.get(), SIGNAL(rangeChanged(qreal,qreal)),
this, SLOT(handleRangeChanged(qreal,qreal)));
QObject::connect(axis(), SIGNAL(reverseChanged(bool)), this, SLOT(handleReverseChanged(bool)));
QObject::connect(axis(), SIGNAL(lineVisibleChanged(bool)),
this, SLOT(handleMinorArrowVisibleChanged(bool)));
QObject::connect(axis(), SIGNAL(linePenChanged(QPen)),
this, SLOT(handleMinorArrowPenChanged(QPen)));
QObject::connect(axis(), SIGNAL(minorGridVisibleChanged(bool)),
this, SLOT(handleMinorGridVisibleChanged(bool)));
QObject::connect(axis(), SIGNAL(minorGridLinePenChanged(QPen)),
this, SLOT(handleMinorGridPenChanged(QPen)));
QObject::connect(axis(), SIGNAL(gridLineColorChanged(QColor)),
this, SLOT(handleGridLineColorChanged(QColor)));
QObject::connect(axis(), SIGNAL(minorGridLineColorChanged(QColor)),
this, SLOT(handleMinorGridLineColorChanged(QColor)));
QObject::connect(axis(), &QAbstractAxis::truncateLabelsChanged,
this, &ChartAxisElement::handleTruncateLabelsChanged);
if (axis()->type() == QAbstractAxis::AxisTypeCategory) {
QCategoryAxis *categoryAxis = static_cast<QCategoryAxis *>(axis());
QObject::connect(categoryAxis,
SIGNAL(labelsPositionChanged(QCategoryAxis::AxisLabelsPosition)),
this, SLOT(handleLabelsPositionChanged()));
}
if (axis()->type() == QAbstractAxis::AxisTypeColor) {
QColorAxis *colorAxis = static_cast<QColorAxis *>(axis());
QObject::connect(colorAxis, &QColorAxis::sizeChanged, this, &ChartAxisElement::handleColorScaleSizeChanged);
QObject::connect(colorAxis, &QColorAxis::gradientChanged, this, &ChartAxisElement::handleColorScaleGradientChanged);
}
}
void ChartAxisElement::handleArrowVisibleChanged(bool visible)
{
m_arrow->setVisible(visible);
}
void ChartAxisElement::handleMinorArrowVisibleChanged(bool visible)
{
m_minorArrow->setVisible(visible);
}
void ChartAxisElement::handleGridVisibleChanged(bool visible)
{
m_grid->setVisible(visible);
}
void ChartAxisElement::handleMinorGridVisibleChanged(bool visible)
{
m_minorGrid->setVisible(visible);
}
void ChartAxisElement::handleLabelsPositionChanged()
{
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
}
void ChartAxisElement::handleTruncateLabelsChanged()
{
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
}
void ChartAxisElement::handleColorScaleSizeChanged()
{
QGraphicsLayoutItem::updateGeometry();
}
void ChartAxisElement::handleColorScaleGradientChanged()
{
const QPixmap &pixmap = m_colorScale->pixmap();
prepareColorScale(pixmap.width(), pixmap.height());
}
void ChartAxisElement::valueLabelEdited(qreal oldValue, qreal newValue)
{
qreal range = max() - min();
qreal center = ((max() - min()) / 2.0) + min();
qreal newRange = 0.0;
auto label = static_cast<ValueAxisLabel *>(this->sender());
if ((oldValue >= center && newValue >= min())
|| (oldValue < center && newValue >= max() && oldValue != min())) {
newRange = range * ((newValue - min()) / (oldValue - min()));
if (newRange > 0) {
m_axis->setRange(min(), min() + newRange);
return;
}
} else if ((oldValue >= center && newValue <= min() && max() != oldValue)
|| (oldValue < center && newValue < max())) {
newRange = range * ((max() - newValue) / (max() - oldValue));
if (newRange > 0) {
m_axis->setRange(max() - newRange, max());
return;
}
}
label->reloadBeforeEditContent();
}
void ChartAxisElement::dateTimeLabelEdited(const QDateTime &oldTime, const QDateTime &newTime)
{
qreal range = max() - min();
qreal center = ((max() - min()) / 2.0) + min();
qreal newRange = 0.0;
qint64 oldValue = oldTime.toMSecsSinceEpoch();
qint64 newValue = newTime.toMSecsSinceEpoch();
if ((oldValue >= center && newValue >= min())
|| (oldValue < center && newValue >= max() && oldValue != min())) {
newRange = range * ((newValue - min()) / (oldValue - min()));
if (newRange > 0) {
m_axis->setRange(
QDateTime::fromMSecsSinceEpoch(min()),
QDateTime::fromMSecsSinceEpoch(min() + newRange));
return;
}
} else if ((oldValue >= center && newValue <= min() && max() != oldValue)
|| (oldValue < center && newValue < max())) {
newRange = range * ((max() - newValue) / (max() - oldValue));
if (newRange > 0) {
m_axis->setRange(max() - newRange, max());
m_axis->setRange(
QDateTime::fromMSecsSinceEpoch(max() - newRange),
QDateTime::fromMSecsSinceEpoch(max()));
return;
}
}
static_cast<DateTimeAxisLabel *>(this->sender())->reloadBeforeEditContent();
}
void ChartAxisElement::handleLabelsVisibleChanged(bool visible)
{
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
m_labels->setVisible(visible);
}
void ChartAxisElement::handleShadesVisibleChanged(bool visible)
{
m_shades->setVisible(visible);
}
void ChartAxisElement::handleTitleVisibleChanged(bool visible)
{
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
m_title->setVisible(visible);
}
void ChartAxisElement::handleLabelsAngleChanged(int angle)
{
const auto items = m_labels->childItems();
for (QGraphicsItem *item : items)
item->setRotation(angle);
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
}
void ChartAxisElement::handleLabelsBrushChanged(const QBrush &brush)
{
const auto items = m_labels->childItems();
for (QGraphicsItem *item : items)
static_cast<QGraphicsTextItem *>(item)->setDefaultTextColor(brush.color());
}
void ChartAxisElement::handleLabelsFontChanged(const QFont &font)
{
const auto items = m_labels->childItems();
for (QGraphicsItem *item : items)
static_cast<QGraphicsTextItem *>(item)->setFont(font);
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
}
void ChartAxisElement::handleTitleTextChanged(const QString &title)
{
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
if (title.isEmpty() || !m_title->isVisible())
m_title->setHtml(title);
}
void ChartAxisElement::handleTitleBrushChanged(const QBrush &brush)
{
m_title->setDefaultTextColor(brush.color());
}
void ChartAxisElement::handleTitleFontChanged(const QFont &font)
{
if (m_title->font() != font) {
m_title->setFont(font);
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
}
}
void ChartAxisElement::handleVisibleChanged(bool visible)
{
setVisible(visible);
if (!visible) {
m_grid->setVisible(visible);
m_arrow->setVisible(visible);
m_minorGrid->setVisible(visible);
m_minorArrow->setVisible(visible);
m_shades->setVisible(visible);
m_labels->setVisible(visible);
m_title->setVisible(visible);
if (m_colorScale)
m_colorScale->setVisible(visible);
} else {
m_grid->setVisible(axis()->isGridLineVisible());
m_arrow->setVisible(axis()->isLineVisible());
m_minorGrid->setVisible(axis()->isMinorGridLineVisible());
m_minorArrow->setVisible(axis()->isLineVisible());
m_shades->setVisible(axis()->shadesVisible());
m_labels->setVisible(axis()->labelsVisible());
m_title->setVisible(axis()->isTitleVisible());
}
if (presenter()) {
if (visible) {
QSizeF before = effectiveSizeHint(Qt::PreferredSize);
QSizeF after = sizeHint(Qt::PreferredSize);
if (before != after)
QGraphicsLayoutItem::updateGeometry();
}
presenter()->layout()->invalidate();
}
}
void ChartAxisElement::handleRangeChanged(qreal min, qreal max)
{
Q_UNUSED(min);
Q_UNUSED(max);
if (!emptyAxis()) {
const QList<qreal> layout = calculateLayout();
updateLayout(layout);
QSizeF before = effectiveSizeHint(Qt::PreferredSize);
QSizeF after = sizeHint(Qt::PreferredSize);
if (before != after) {
QGraphicsLayoutItem::updateGeometry();
// We don't want to call invalidate on layout, since it will change minimum size of
// component, which we would like to avoid since it causes nasty flips when scrolling
// or zooming, instead recalculate layout and use plotArea for extra space.
presenter()->layout()->setGeometry(presenter()->layout()->geometry());
}
}
}
void ChartAxisElement::handleReverseChanged(bool reverse)
{
Q_UNUSED(reverse);
QGraphicsLayoutItem::updateGeometry();
presenter()->layout()->invalidate();
}
bool ChartAxisElement::emptyAxis() const
{
return axisGeometry().isEmpty()
|| gridGeometry().isEmpty()
|| qFuzzyIsNull(max() - min());
}
qreal ChartAxisElement::min() const
{
return m_axis->d_ptr->min();
}
qreal ChartAxisElement::max() const
{
return m_axis->d_ptr->max();
}
qreal ChartAxisElement::tickInterval() const
{
QValueAxis *valueAxis = qobject_cast<QValueAxis *>(m_axis);
if (valueAxis)
return valueAxis->tickInterval();
else
return 0.0;
}
qreal ChartAxisElement::tickAnchor() const
{
QValueAxis *valueAxis = qobject_cast<QValueAxis *>(m_axis);
if (valueAxis)
return valueAxis->tickAnchor();
else
return 0.0;
}
QString ChartAxisElement::formatLabel(const QString &formatSpec, const QByteArray &array,
qreal value, int precision, const QString &preStr,
const QString &postStr) const
{
QString retVal;
if (!formatSpec.isEmpty()) {
if (formatSpec.at(0) == QLatin1Char('d')
|| formatSpec.at(0) == QLatin1Char('i')
|| formatSpec.at(0) == QLatin1Char('c')) {
if (presenter()->localizeNumbers())
retVal = preStr + presenter()->locale().toString(qint64(value)) + postStr;
else
retVal = QString::asprintf(array.constData(), qint64(value));
} else if (formatSpec.at(0) == QLatin1Char('u')
|| formatSpec.at(0) == QLatin1Char('o')
|| formatSpec.at(0) == QLatin1Char('x')
|| formatSpec.at(0) == QLatin1Char('X')) {
// These formats are not supported by localized numbers
retVal = QString::asprintf(array.constData(), quint64(value));
} else if (formatSpec.at(0) == QLatin1Char('f')
|| formatSpec.at(0) == QLatin1Char('F')
|| formatSpec.at(0) == QLatin1Char('e')
|| formatSpec.at(0) == QLatin1Char('E')
|| formatSpec.at(0) == QLatin1Char('g')
|| formatSpec.at(0) == QLatin1Char('G')) {
if (presenter()->localizeNumbers()) {
retVal = preStr
+ presenter()->locale().toString(value, formatSpec.at(0).toLatin1(),
precision)
+ postStr;
} else {
retVal = QString::asprintf(array.constData(), value);
}
}
}
return retVal;
}
void ChartAxisElement::prepareColorScale(const qreal width, const qreal height)
{
if (axis()->type() == QAbstractAxis::AxisTypeColor) {
QColorAxis *colorAxis = static_cast<QColorAxis *>(axis());
if (colorAxis->gradient() != QLinearGradient() && width != 0 && height != 0) {
m_colorScale->setVisible(true);
QImage image(width, height, QImage::Format_ARGB32);
QPainter painter(&image);
QLinearGradient gradient;
if (colorAxis->orientation() == Qt::Horizontal) {
gradient = QLinearGradient(QPointF(0, 0), QPointF(width, 0));
const auto &stops = colorAxis->gradient().stops();
for (const auto &stop : stops)
gradient.setColorAt(stop.first, stop.second);
} else {
gradient = QLinearGradient(QPointF(0, 0), QPointF(0, height));
const auto &stops = colorAxis->gradient().stops();
for (int i = stops.size() - 1; i >= 0; --i) {
const auto &stop = stops.at(i);
gradient.setColorAt(1 - stop.first, stop.second);
}
}
painter.fillRect(image.rect(), gradient);
painter.setPen(axis()->linePen());
painter.drawRect(image.rect());
const QPixmap &pixmap = QPixmap::fromImage(image);
m_colorScale->setPixmap(pixmap);
}
}
}
static int precisionDigits(qreal min, qreal max, int ticks)
{
// How many digits of each tick value should we display after the decimal point ?
// For example tick marks 1.002 and 1.003 have a difference of 0.001 and need 3 decimals.
if (ticks > 1) {
// Number of digits after decimal that *don't* change between ticks:
const int gap = -qFloor(std::log10((max - min) / (ticks - 1)));
if (gap > 0)
return gap + 1;
}
// We want at least one digit after the decimal point even when digits
// before are changing, or when we only have a single tick-mark:
return 1;
}
QStringList ChartAxisElement::createValueLabels(qreal min, qreal max, int ticks,
qreal tickInterval, qreal tickAnchor,
QValueAxis::TickType tickType,
const QString &format) const
{
QStringList labels;
if (max <= min || ticks < 1)
return labels;
if (format.isEmpty()) {
const int n = precisionDigits(min, max, ticks);
if (tickType == QValueAxis::TicksFixed) {
for (int i = 0; i < ticks; i++) {
qreal value = min + (i * (max - min) / (ticks - 1));
labels << presenter()->numberToString(value, 'f', n);
}
} else {
const qreal ticksFromAnchor = (tickAnchor - min) / tickInterval;
const qreal firstMajorTick = tickAnchor - std::floor(ticksFromAnchor) * tickInterval;
qreal value = firstMajorTick;
while (value <= max) {
labels << presenter()->numberToString(value, 'f', n);
value += tickInterval;
}
}
} else {
QByteArray array = format.toLatin1();
QString formatSpec;
QString preStr;
QString postStr;
int precision = 6; // Six is the default precision in Qt API
if (presenter()->localizeNumbers()) {
QRegularExpressionMatch rmatch;
if (format.indexOf(labelFormatMatcherLocalized(), 0, &rmatch) != -1) {
preStr = rmatch.captured(1);
if (!rmatch.captured(2).isEmpty())
precision = rmatch.captured(2).toInt();
formatSpec = rmatch.captured(3);
postStr = rmatch.captured(4);
}
} else {
QRegularExpressionMatch rmatch;
if (format.indexOf(labelFormatMatcher(), 0, &rmatch) != -1)
formatSpec = rmatch.captured(1);
}
if (tickType == QValueAxis::TicksFixed) {
for (int i = 0; i < ticks; i++) {
qreal value = min + (i * (max - min) / (ticks - 1));
labels << formatLabel(formatSpec, array, value, precision, preStr, postStr);
}
} else {
const qreal ticksFromAnchor = (tickAnchor - min) / tickInterval;
const qreal firstMajorTick = tickAnchor - std::floor(ticksFromAnchor) * tickInterval;
qreal value = firstMajorTick;
while (value <= max) {
labels << formatLabel(formatSpec, array, value, precision, preStr, postStr);
value += tickInterval;
}
}
}
return labels;
}
QStringList ChartAxisElement::createLogValueLabels(qreal min, qreal max, qreal base, int ticks,
const QString &format) const
{
QStringList labels;
if (max <= min || ticks < 1)
return labels;
const int firstTick = qCeil(qLn(base > 1 ? min : max) / qLn(base));
if (format.isEmpty()) {
const int n = precisionDigits(min, max, ticks);
for (int i = firstTick; i < ticks + firstTick; i++) {
qreal value = qPow(base, i);
labels << presenter()->numberToString(value, 'f', n);
}
} else {
QByteArray array = format.toLatin1();
QString formatSpec;
QString preStr;
QString postStr;
int precision = 6; // Six is the default precision in Qt API
if (presenter()->localizeNumbers()) {
QRegularExpressionMatch rmatch;
if (format.indexOf(labelFormatMatcherLocalized(), 0, &rmatch) != -1) {
preStr = rmatch.captured(1);
if (!rmatch.captured(2).isEmpty())
precision = rmatch.captured(2).toInt();
formatSpec = rmatch.captured(3);
postStr = rmatch.captured(4);
}
} else {
QRegularExpressionMatch rmatch;
if (format.indexOf(labelFormatMatcher(), 0, &rmatch) != -1)
formatSpec = rmatch.captured(1);
}
for (int i = firstTick; i < ticks + firstTick; i++) {
qreal value = qPow(base, i);
labels << formatLabel(formatSpec, array, value, precision, preStr, postStr);
}
}
return labels;
}
QStringList ChartAxisElement::createDateTimeLabels(qreal min, qreal max,int ticks,
const QString &format) const
{
QStringList labels;
if (max <= min || ticks < 1)
return labels;
for (int i = 0; i < ticks; i++) {
qreal value = min + (i * (max - min) / (ticks - 1));
labels << presenter()->locale().toString(QDateTime::fromMSecsSinceEpoch(value), format);
}
return labels;
}
QStringList ChartAxisElement::createColorLabels(qreal min, qreal max, int ticks) const
{
QStringList labels;
if (max <= min || ticks < 1)
return labels;
const int n = precisionDigits(min, max, ticks);
for (int i = 0; i < ticks; ++i) {
qreal value = min + (i * (max - min) / (ticks - 1));
labels << presenter()->numberToString(value, 'f', n);
}
return labels;
}
bool ChartAxisElement::labelsEditable() const
{
return m_labelsEditable;
}
void ChartAxisElement::setLabelsEditable(bool labelsEditable)
{
if (axis()->type() == QAbstractAxis::AxisTypeValue
|| axis()->type() == QAbstractAxis::AxisTypeDateTime) {
labelGroup()->setHandlesChildEvents(!labelsEditable);
const QList<QGraphicsItem *> childItems = labelGroup()->childItems();
for (auto item : childItems) {
switch (axis()->type()) {
case QAbstractAxis::AxisTypeValue:
static_cast<ValueAxisLabel *>(item)->setEditable(labelsEditable);
break;
case QAbstractAxis::AxisTypeDateTime:
static_cast<DateTimeAxisLabel *>(item)->setEditable(labelsEditable);
break;
default:
break;
}
}
m_labelsEditable = labelsEditable;
}
}
bool ChartAxisElement::labelsVisible() const
{
return m_labels->isVisible();
}
void ChartAxisElement::axisSelected()
{
emit clicked();
}
QT_END_NAMESPACE
#include "moc_chartaxiselement_p.cpp"