File indexing completed on 2025-02-09 04:26:00
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"