File indexing completed on 2024-10-06 12:23:47
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"