File indexing completed on 2024-04-28 03:56:48

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 
0020 static const float PixelsPerStep = 2.0;
0021 
0022 
0023 QList<QVector2D> interpolatePoints(const QList<QVector2D> &points, float height);
0024 QList<float> calculateTangents(const QList<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::interpolate() const
0112 {
0113     return m_interpolate;
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::setInterpolate(bool newInterpolate)
0127 {
0128     if (newInterpolate == m_interpolate) {
0129         return;
0130     }
0131 
0132     m_interpolate = newInterpolate;
0133     polish();
0134     Q_EMIT interpolateChanged();
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     QList<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         QList<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_interpolate) {
0254             m_values[valueSource] = interpolatePoints(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 void LineChart::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
0309 {
0310     XYChart::geometryChange(newGeometry, oldGeometry);
0311     if (newGeometry != oldGeometry) {
0312         polish();
0313     }
0314 }
0315 
0316 void LineChart::updateLineNode(LineChartNode *node, const QColor &lineColor, const QColor &fillColor, ChartDataSource *valueSource)
0317 {
0318     if (window()) {
0319         node->setRect(boundingRect(), window()->devicePixelRatio());
0320     } else {
0321         node->setRect(boundingRect(), 1.0);
0322     }
0323     node->setLineColor(lineColor);
0324     node->setFillColor(fillColor);
0325     node->setLineWidth(m_lineWidth);
0326 
0327     auto values = m_values.value(valueSource);
0328     node->setValues(values);
0329 
0330     node->updatePoints();
0331 }
0332 
0333 void LineChart::createPointDelegates(const QList<QVector2D> &values, int sourceIndex)
0334 {
0335     auto valueSource = valueSources().at(sourceIndex);
0336 
0337     QList<QQuickItem *> delegates;
0338     for (int i = 0; i < values.size(); ++i) {
0339         auto delegate = qobject_cast<QQuickItem *>(m_pointDelegate->beginCreate(qmlContext(m_pointDelegate)));
0340         if (!delegate) {
0341             qWarning() << "Delegate creation for point" << i << "of value source" << valueSource->objectName()
0342                        << "failed, make sure pointDelegate is a QQuickItem";
0343             delegate = new QQuickItem(this);
0344         }
0345 
0346         delegate->setParent(this);
0347         delegate->setParentItem(this);
0348         updatePointDelegate(delegate, values.at(i), valueSource->item(i), sourceIndex);
0349 
0350         m_pointDelegate->completeCreate();
0351 
0352         delegates.append(delegate);
0353     }
0354 
0355     m_pointDelegates.insert(valueSource, delegates);
0356 }
0357 
0358 void LineChart::updatePointDelegate(QQuickItem *delegate, const QVector2D &position, const QVariant &value, int sourceIndex)
0359 {
0360     auto pos = QPointF{position.x() - delegate->width() / 2, (1.0 - position.y()) * height() - delegate->height() / 2};
0361     delegate->setPosition(pos);
0362 
0363     auto attached = static_cast<LineChartAttached *>(qmlAttachedPropertiesObject<LineChart>(delegate, true));
0364     attached->setValue(value);
0365     attached->setColor(colorSource() ? colorSource()->item(sourceIndex).value<QColor>() : Qt::black);
0366     attached->setName(nameSource() ? nameSource()->item(sourceIndex).toString() : QString{});
0367     attached->setShortName(shortNameSource() ? shortNameSource()->item(sourceIndex).toString() : QString{});
0368 }
0369 
0370 // Smoothly interpolate between points, using monotonic cubic interpolation.
0371 QList<QVector2D> interpolatePoints(const QList<QVector2D> &points, float height)
0372 {
0373     if (points.size() < 2) {
0374         return points;
0375     }
0376 
0377     auto tangents = calculateTangents(points, height);
0378 
0379     QList<QVector2D> result;
0380 
0381     auto current = QVector2D{0.0, points.first().y() * height};
0382     result.append(QVector2D{0.0, points.first().y()});
0383 
0384     for (int i = 0; i < points.size() - 1; ++i) {
0385         auto next = QVector2D{points.at(i + 1).x(), points.at(i + 1).y() * height};
0386 
0387         auto currentTangent = tangents.at(i);
0388         auto nextTangent = tangents.at(i + 1);
0389 
0390         auto stepCount = int(std::max(1.0f, (next.x() - current.x()) / PixelsPerStep));
0391         auto stepSize = (next.x() - current.x()) / stepCount;
0392 
0393         if (stepCount == 1 || qFuzzyIsNull(next.y() - current.y())) {
0394             result.append(QVector2D{next.x(), next.y() / height});
0395             current = next;
0396             continue;
0397         }
0398 
0399         for (auto delta = current.x(); delta < next.x(); delta += stepSize) {
0400             auto interpolated = cubicHermite(current, next, delta, currentTangent, nextTangent);
0401             interpolated.setY(interpolated.y() / height);
0402             result.append(interpolated);
0403         }
0404 
0405         current = next;
0406     }
0407 
0408     current.setY(current.y() / height);
0409     result.append(current);
0410 
0411     return result;
0412 }
0413 
0414 // This calculates the tangents for monotonic cubic spline interpolation.
0415 // See https://en.wikipedia.org/wiki/Monotone_cubic_interpolation for details.
0416 QList<float> calculateTangents(const QList<QVector2D> &points, float height)
0417 {
0418     QList<float> secantSlopes;
0419     secantSlopes.reserve(points.size());
0420 
0421     QList<float> tangents;
0422     tangents.reserve(points.size());
0423 
0424     float previousSlope = 0.0;
0425     float slope = 0.0;
0426 
0427     for (int i = 0; i < points.size() - 1; ++i) {
0428         auto current = points.at(i);
0429         auto next = points.at(i + 1);
0430 
0431         previousSlope = slope;
0432         slope = (next.y() * height - current.y() * height) / (next.x() - current.x());
0433 
0434         secantSlopes.append(slope);
0435 
0436         if (i == 0) {
0437             tangents.append(slope);
0438         } else if (previousSlope * slope < 0.0) {
0439             tangents.append(0.0);
0440         } else {
0441             tangents.append((previousSlope + slope) / 2.0);
0442         }
0443     }
0444     tangents.append(secantSlopes.last());
0445 
0446     for (int i = 0; i < points.size() - 1; ++i) {
0447         auto slope = secantSlopes.at(i);
0448 
0449         if (qFuzzyIsNull(slope)) {
0450             tangents[i] = 0.0;
0451             tangents[i + 1] = 0.0;
0452             continue;
0453         }
0454 
0455         auto alpha = tangents.at(i) / slope;
0456         auto beta = tangents.at(i + 1) / slope;
0457 
0458         if (alpha < 0.0) {
0459             tangents[i] = 0.0;
0460         }
0461 
0462         if (beta < 0.0) {
0463             tangents[i + 1] = 0.0;
0464         }
0465 
0466         auto length = alpha * alpha + beta * beta;
0467         if (length > 9) {
0468             auto tau = 3.0 / sqrt(length);
0469             tangents[i] = tau * alpha * slope;
0470             tangents[i + 1] = tau * beta * slope;
0471         }
0472     }
0473 
0474     return tangents;
0475 }
0476 
0477 // Cubic Hermite Interpolation between two points
0478 // Given two points, an X value between those two points and two tangents, this
0479 // will perform cubic hermite interpolation between the two points.
0480 // See https://en.wikipedia.org/wiki/Cubic_Hermite_spline for details as well as
0481 // the above mentioned article on monotonic interpolation.
0482 QVector2D cubicHermite(const QVector2D &first, const QVector2D &second, float step, float mFirst, float mSecond)
0483 {
0484     const auto delta = second.x() - first.x();
0485     const auto t = (step - first.x()) / delta;
0486 
0487     // Hermite basis values
0488     // h₀₀(t) = 2t³ - 3t² + 1
0489     const auto h00 = 2.0f * std::pow(t, 3.0f) - 3.0f * std::pow(t, 2.0f) + 1.0f;
0490     // h₁₀(t) = t³ - 2t² + t
0491     const auto h10 = std::pow(t, 3.0f) - 2.0f * std::pow(t, 2.0f) + t;
0492     // h₀₁(t) = -2t³ + 3t²
0493     const auto h01 = -2.0f * std::pow(t, 3.0f) + 3.0f * std::pow(t, 2.0f);
0494     // h₁₁(t) = t³ - t²
0495     const auto h11 = std::pow(t, 3.0f) - std::pow(t, 2.0f);
0496 
0497     auto result = QVector2D{step, first.y() * h00 + delta * mFirst * h10 + second.y() * h01 + delta * mSecond * h11};
0498     return result;
0499 }
0500 
0501 #include "moc_LineChart.cpp"