File indexing completed on 2024-04-14 14:27:07

0001 /*
0002  * This file is part of KQuickCharts
0003  * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006  */
0007 
0008 #include "LineChart.h"
0009 
0010 #include <cmath>
0011 
0012 #include <QPainter>
0013 #include <QPainterPath>
0014 #include <QQuickWindow>
0015 
0016 #include "RangeGroup.h"
0017 #include "datasource/ChartDataSource.h"
0018 #include "scenegraph/LineChartNode.h"
0019 #include "scenegraph/LineGridNode.h"
0020 
0021 static const float PixelsPerStep = 2.0;
0022 
0023 QVector<QVector2D> interpolate(const QVector<QVector2D> &points, float height);
0024 QVector<float> calculateTangents(const QVector<QVector2D> &points, float height);
0025 QVector2D cubicHermite(const QVector2D &first, const QVector2D &second, float step, float mFirst, float mSecond);
0026 
0027 QColor colorWithAlpha(const QColor &color, qreal opacity)
0028 {
0029     auto result = color;
0030     result.setRedF(result.redF() * opacity);
0031     result.setGreenF(result.greenF() * opacity);
0032     result.setBlueF(result.blueF() * opacity);
0033     result.setAlphaF(opacity);
0034     return result;
0035 }
0036 
0037 LineChartAttached::LineChartAttached(QObject *parent)
0038     : QObject(parent)
0039 {
0040 }
0041 
0042 QVariant LineChartAttached::value() const
0043 {
0044     return m_value;
0045 }
0046 
0047 void LineChartAttached::setValue(const QVariant &value)
0048 {
0049     if (value == m_value) {
0050         return;
0051     }
0052 
0053     m_value = value;
0054     Q_EMIT valueChanged();
0055 }
0056 
0057 QColor LineChartAttached::color() const
0058 {
0059     return m_color;
0060 }
0061 
0062 void LineChartAttached::setColor(const QColor &color)
0063 {
0064     if (color == m_color) {
0065         return;
0066     }
0067 
0068     m_color = color;
0069     Q_EMIT colorChanged();
0070 }
0071 
0072 QString LineChartAttached::name() const
0073 {
0074     return m_name;
0075 }
0076 
0077 void LineChartAttached::setName(const QString &newName)
0078 {
0079     if (newName == m_name) {
0080         return;
0081     }
0082 
0083     m_name = newName;
0084     Q_EMIT nameChanged();
0085 }
0086 
0087 QString LineChartAttached::shortName() const
0088 {
0089     if (m_shortName.isEmpty()) {
0090         return m_name;
0091     } else {
0092         return m_shortName;
0093     }
0094 }
0095 
0096 void LineChartAttached::setShortName(const QString &newShortName)
0097 {
0098     if (newShortName == m_shortName) {
0099         return;
0100     }
0101 
0102     m_shortName = newShortName;
0103     Q_EMIT shortNameChanged();
0104 }
0105 
0106 LineChart::LineChart(QQuickItem *parent)
0107     : XYChart(parent)
0108 {
0109 }
0110 
0111 bool LineChart::smooth() const
0112 {
0113     return m_smooth;
0114 }
0115 
0116 qreal LineChart::lineWidth() const
0117 {
0118     return m_lineWidth;
0119 }
0120 
0121 qreal LineChart::fillOpacity() const
0122 {
0123     return m_fillOpacity;
0124 }
0125 
0126 void LineChart::setSmooth(bool smooth)
0127 {
0128     if (smooth == m_smooth) {
0129         return;
0130     }
0131 
0132     m_smooth = smooth;
0133     polish();
0134     Q_EMIT smoothChanged();
0135 }
0136 
0137 void LineChart::setLineWidth(qreal width)
0138 {
0139     if (qFuzzyCompare(m_lineWidth, width)) {
0140         return;
0141     }
0142 
0143     m_lineWidth = width;
0144     update();
0145     Q_EMIT lineWidthChanged();
0146 }
0147 
0148 void LineChart::setFillOpacity(qreal opacity)
0149 {
0150     if (qFuzzyCompare(m_fillOpacity, opacity)) {
0151         return;
0152     }
0153 
0154     m_fillOpacity = opacity;
0155     update();
0156     Q_EMIT fillOpacityChanged();
0157 }
0158 
0159 ChartDataSource *LineChart::fillColorSource() const
0160 {
0161     return m_fillColorSource;
0162 }
0163 
0164 void LineChart::setFillColorSource(ChartDataSource *newFillColorSource)
0165 {
0166     if (newFillColorSource == m_fillColorSource) {
0167         return;
0168     }
0169 
0170     m_fillColorSource = newFillColorSource;
0171     update();
0172     Q_EMIT fillColorSourceChanged();
0173 }
0174 
0175 QQmlComponent *LineChart::pointDelegate() const
0176 {
0177     return m_pointDelegate;
0178 }
0179 
0180 void LineChart::setPointDelegate(QQmlComponent *newPointDelegate)
0181 {
0182     if (newPointDelegate == m_pointDelegate) {
0183         return;
0184     }
0185 
0186     m_pointDelegate = newPointDelegate;
0187     for (auto entry : std::as_const(m_pointDelegates)) {
0188         qDeleteAll(entry);
0189     }
0190     m_pointDelegates.clear();
0191     polish();
0192     Q_EMIT pointDelegateChanged();
0193 }
0194 
0195 void LineChart::updatePolish()
0196 {
0197     if (m_rangeInvalid) {
0198         updateComputedRange();
0199         m_rangeInvalid = false;
0200     }
0201 
0202     QVector<QVector2D> previousValues;
0203 
0204     const auto range = computedRange();
0205     const auto sources = valueSources();
0206     for (int i = 0; i < sources.size(); ++i) {
0207         auto valueSource = sources.at(i);
0208 
0209         float stepSize = width() / (range.distanceX - 1);
0210         QVector<QVector2D> values(range.distanceX);
0211         auto generator = [&, i = range.startX]() mutable -> QVector2D {
0212             float value = 0;
0213             if (range.distanceY != 0) {
0214                 value = (valueSource->item(i).toFloat() - range.startY) / range.distanceY;
0215             }
0216 
0217             auto result = QVector2D{direction() == Direction::ZeroAtStart ? i * stepSize : float(boundingRect().right()) - i * stepSize, value};
0218             i++;
0219             return result;
0220         };
0221 
0222         if (direction() == Direction::ZeroAtStart) {
0223             std::generate_n(values.begin(), range.distanceX, generator);
0224         } else {
0225             std::generate_n(values.rbegin(), range.distanceX, generator);
0226         }
0227 
0228         if (stacked() && !previousValues.isEmpty()) {
0229             if (values.size() != previousValues.size()) {
0230                 qWarning() << "Value source" << valueSource->objectName()
0231                            << "has a different number of elements from the previous source. Ignoring stacking for this source.";
0232             } else {
0233                 std::for_each(values.begin(), values.end(), [previousValues, i = 0](QVector2D &point) mutable {
0234                     point.setY(point.y() + previousValues.at(i++).y());
0235                 });
0236             }
0237         }
0238         previousValues = values;
0239 
0240         if (m_pointDelegate) {
0241             auto &delegates = m_pointDelegates[valueSource];
0242             if (delegates.size() != values.size()) {
0243                 qDeleteAll(delegates);
0244                 createPointDelegates(values, i);
0245             } else {
0246                 for (int item = 0; item < values.size(); ++item) {
0247                     auto delegate = delegates.at(item);
0248                     updatePointDelegate(delegate, values.at(item), valueSource->item(item), i);
0249                 }
0250             }
0251         }
0252 
0253         if (m_smooth) {
0254             m_values[valueSource] = interpolate(values, height());
0255         } else {
0256             m_values[valueSource] = values;
0257         }
0258     }
0259 
0260     const auto pointKeys = m_pointDelegates.keys();
0261     for (auto key : pointKeys) {
0262         if (!sources.contains(key)) {
0263             qDeleteAll(m_pointDelegates[key]);
0264             m_pointDelegates.remove(key);
0265         }
0266     }
0267 
0268     update();
0269 }
0270 
0271 QSGNode *LineChart::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data)
0272 {
0273     Q_UNUSED(data);
0274 
0275     if (!node) {
0276         node = new QSGNode();
0277     }
0278 
0279     const auto sources = valueSources();
0280     for (int i = 0; i < sources.size(); ++i) {
0281         int childIndex = sources.size() - 1 - i;
0282         while (childIndex >= node->childCount()) {
0283             node->appendChildNode(new LineChartNode{});
0284         }
0285         auto lineNode = static_cast<LineChartNode *>(node->childAtIndex(childIndex));
0286         auto color = colorSource() ? colorSource()->item(i).value<QColor>() : Qt::black;
0287         auto fillColor = m_fillColorSource ? m_fillColorSource->item(i).value<QColor>() : colorWithAlpha(color, m_fillOpacity);
0288         updateLineNode(lineNode, color, fillColor, sources.at(i));
0289     }
0290 
0291     while (node->childCount() > sources.size()) {
0292         // removeChildNode unfortunately does not take care of deletion so we
0293         // need to handle this manually.
0294         auto lastNode = node->childAtIndex(node->childCount() - 1);
0295         node->removeChildNode(lastNode);
0296         delete lastNode;
0297     }
0298 
0299     return node;
0300 }
0301 
0302 void LineChart::onDataChanged()
0303 {
0304     m_rangeInvalid = true;
0305     polish();
0306 }
0307 
0308 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0309 void LineChart::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
0310 #else
0311 void LineChart::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
0312 #endif
0313 {
0314 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0315     XYChart::geometryChanged(newGeometry, oldGeometry);
0316 #else
0317     XYChart::geometryChange(newGeometry, oldGeometry);
0318 #endif
0319     if (newGeometry != oldGeometry) {
0320         polish();
0321     }
0322 }
0323 
0324 void LineChart::updateLineNode(LineChartNode *node, const QColor &lineColor, const QColor &fillColor, ChartDataSource *valueSource)
0325 {
0326     if (window()) {
0327         node->setRect(boundingRect(), window()->devicePixelRatio());
0328     } else {
0329         node->setRect(boundingRect(), 1.0);
0330     }
0331     node->setLineColor(lineColor);
0332     node->setFillColor(fillColor);
0333     node->setLineWidth(m_lineWidth);
0334 
0335     auto values = m_values.value(valueSource);
0336     node->setValues(values);
0337 
0338     node->updatePoints();
0339 }
0340 
0341 void LineChart::createPointDelegates(const QVector<QVector2D> &values, int sourceIndex)
0342 {
0343     auto valueSource = valueSources().at(sourceIndex);
0344 
0345     QVector<QQuickItem *> delegates;
0346     for (int i = 0; i < values.size(); ++i) {
0347         auto delegate = qobject_cast<QQuickItem *>(m_pointDelegate->beginCreate(qmlContext(m_pointDelegate)));
0348         if (!delegate) {
0349             qWarning() << "Delegate creation for point" << i << "of value source" << valueSource->objectName()
0350                        << "failed, make sure pointDelegate is a QQuickItem";
0351             delegate = new QQuickItem(this);
0352         }
0353 
0354         delegate->setParent(this);
0355         delegate->setParentItem(this);
0356         updatePointDelegate(delegate, values.at(i), valueSource->item(i), sourceIndex);
0357 
0358         m_pointDelegate->completeCreate();
0359 
0360         delegates.append(delegate);
0361     }
0362 
0363     m_pointDelegates.insert(valueSource, delegates);
0364 }
0365 
0366 void LineChart::updatePointDelegate(QQuickItem *delegate, const QVector2D &position, const QVariant &value, int sourceIndex)
0367 {
0368     auto pos = QPointF{position.x() - delegate->width() / 2, (1.0 - position.y()) * height() - delegate->height() / 2};
0369     delegate->setPosition(pos);
0370 
0371     auto attached = static_cast<LineChartAttached *>(qmlAttachedPropertiesObject<LineChart>(delegate, true));
0372     attached->setValue(value);
0373     attached->setColor(colorSource() ? colorSource()->item(sourceIndex).value<QColor>() : Qt::black);
0374     attached->setName(nameSource() ? nameSource()->item(sourceIndex).toString() : QString{});
0375     attached->setShortName(shortNameSource() ? shortNameSource()->item(sourceIndex).toString() : QString{});
0376 }
0377 
0378 // Smoothly interpolate between points, using monotonic cubic interpolation.
0379 QVector<QVector2D> interpolate(const QVector<QVector2D> &points, float height)
0380 {
0381     if (points.size() < 2) {
0382         return points;
0383     }
0384 
0385     auto tangents = calculateTangents(points, height);
0386 
0387     QVector<QVector2D> result;
0388 
0389     auto current = QVector2D{0.0, points.first().y() * height};
0390     result.append(QVector2D{0.0, points.first().y()});
0391 
0392     for (int i = 0; i < points.size() - 1; ++i) {
0393         auto next = QVector2D{points.at(i + 1).x(), points.at(i + 1).y() * height};
0394 
0395         auto currentTangent = tangents.at(i);
0396         auto nextTangent = tangents.at(i + 1);
0397 
0398         auto stepCount = int(std::max(1.0f, (next.x() - current.x()) / PixelsPerStep));
0399         auto stepSize = (next.x() - current.x()) / stepCount;
0400 
0401         if (stepCount == 1 || qFuzzyIsNull(next.y() - current.y())) {
0402             result.append(QVector2D{next.x(), next.y() / height});
0403             current = next;
0404             continue;
0405         }
0406 
0407         for (auto delta = current.x(); delta < next.x(); delta += stepSize) {
0408             auto interpolated = cubicHermite(current, next, delta, currentTangent, nextTangent);
0409             interpolated.setY(interpolated.y() / height);
0410             result.append(interpolated);
0411         }
0412 
0413         current = next;
0414     }
0415 
0416     current.setY(current.y() / height);
0417     result.append(current);
0418 
0419     return result;
0420 }
0421 
0422 // This calculates the tangents for monotonic cubic spline interpolation.
0423 // See https://en.wikipedia.org/wiki/Monotone_cubic_interpolation for details.
0424 QVector<float> calculateTangents(const QVector<QVector2D> &points, float height)
0425 {
0426     QVector<float> secantSlopes;
0427     secantSlopes.reserve(points.size());
0428 
0429     QVector<float> tangents;
0430     tangents.reserve(points.size());
0431 
0432     float previousSlope = 0.0;
0433     float slope = 0.0;
0434 
0435     for (int i = 0; i < points.size() - 1; ++i) {
0436         auto current = points.at(i);
0437         auto next = points.at(i + 1);
0438 
0439         previousSlope = slope;
0440         slope = (next.y() * height - current.y() * height) / (next.x() - current.x());
0441 
0442         secantSlopes.append(slope);
0443 
0444         if (i == 0) {
0445             tangents.append(slope);
0446         } else if (previousSlope * slope < 0.0) {
0447             tangents.append(0.0);
0448         } else {
0449             tangents.append((previousSlope + slope) / 2.0);
0450         }
0451     }
0452     tangents.append(secantSlopes.last());
0453 
0454     for (int i = 0; i < points.size() - 1; ++i) {
0455         auto slope = secantSlopes.at(i);
0456 
0457         if (qFuzzyIsNull(slope)) {
0458             tangents[i] = 0.0;
0459             tangents[i + 1] = 0.0;
0460             continue;
0461         }
0462 
0463         auto alpha = tangents.at(i) / slope;
0464         auto beta = tangents.at(i + 1) / slope;
0465 
0466         if (alpha < 0.0) {
0467             tangents[i] = 0.0;
0468         }
0469 
0470         if (beta < 0.0) {
0471             tangents[i + 1] = 0.0;
0472         }
0473 
0474         auto length = alpha * alpha + beta * beta;
0475         if (length > 9) {
0476             auto tau = 3.0 / sqrt(length);
0477             tangents[i] = tau * alpha * slope;
0478             tangents[i + 1] = tau * beta * slope;
0479         }
0480     }
0481 
0482     return tangents;
0483 }
0484 
0485 // Cubic Hermite Interpolation between two points
0486 // Given two points, an X value between those two points and two tangents, this
0487 // will perform cubic hermite interpolation between the two points.
0488 // See https://en.wikipedia.org/wiki/Cubic_Hermite_spline for details as well as
0489 // the above mentioned article on monotonic interpolation.
0490 QVector2D cubicHermite(const QVector2D &first, const QVector2D &second, float step, float mFirst, float mSecond)
0491 {
0492     const auto delta = second.x() - first.x();
0493     const auto t = (step - first.x()) / delta;
0494 
0495     // Hermite basis values
0496     // h₀₀(t) = 2t³ - 3t² + 1
0497     const auto h00 = 2.0f * std::pow(t, 3.0f) - 3.0f * std::pow(t, 2.0f) + 1.0f;
0498     // h₁₀(t) = t³ - 2t² + t
0499     const auto h10 = std::pow(t, 3.0f) - 2.0f * std::pow(t, 2.0f) + t;
0500     // h₀₁(t) = -2t³ + 3t²
0501     const auto h01 = -2.0f * std::pow(t, 3.0f) + 3.0f * std::pow(t, 2.0f);
0502     // h₁₁(t) = t³ - t²
0503     const auto h11 = std::pow(t, 3.0f) - std::pow(t, 2.0f);
0504 
0505     auto result = QVector2D{step, first.y() * h00 + delta * mFirst * h10 + second.y() * h01 + delta * mSecond * h11};
0506     return result;
0507 }
0508 
0509 #include "moc_LineChart.cpp"