1/*
2 * This file is part of KQuickCharts
3 * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
4 *
5 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7
8#include "LineChart.h"
9
10#include <cmath>
11
12#include <QPainter>
13#include <QPainterPath>
14#include <QQuickWindow>
15
16#include "RangeGroup.h"
17#include "datasource/ChartDataSource.h"
18#include "scenegraph/LineChartNode.h"
19
20static const float PixelsPerStep = 2.0;
21
22
23QList<QVector2D> interpolatePoints(const QList<QVector2D> &points, float height);
24QList<float> calculateTangents(const QList<QVector2D> &points, float height);
25QVector2D cubicHermite(const QVector2D &first, const QVector2D &second, float step, float mFirst, float mSecond);
26
27QColor colorWithAlpha(const QColor &color, qreal opacity)
28{
29 auto result = color;
30 result.setRedF(result.redF() * opacity);
31 result.setGreenF(result.greenF() * opacity);
32 result.setBlueF(result.blueF() * opacity);
33 result.setAlphaF(opacity);
34 return result;
35}
36
37LineChartAttached::LineChartAttached(QObject *parent)
38 : QObject(parent)
39{
40}
41
42QVariant LineChartAttached::value() const
43{
44 return m_value;
45}
46
47void LineChartAttached::setValue(const QVariant &value)
48{
49 if (value == m_value) {
50 return;
51 }
52
53 m_value = value;
54 Q_EMIT valueChanged();
55}
56
57QColor LineChartAttached::color() const
58{
59 return m_color;
60}
61
62void LineChartAttached::setColor(const QColor &color)
63{
64 if (color == m_color) {
65 return;
66 }
67
68 m_color = color;
69 Q_EMIT colorChanged();
70}
71
72QString LineChartAttached::name() const
73{
74 return m_name;
75}
76
77void LineChartAttached::setName(const QString &newName)
78{
79 if (newName == m_name) {
80 return;
81 }
82
83 m_name = newName;
84 Q_EMIT nameChanged();
85}
86
87QString LineChartAttached::shortName() const
88{
89 if (m_shortName.isEmpty()) {
90 return m_name;
91 } else {
92 return m_shortName;
93 }
94}
95
96void LineChartAttached::setShortName(const QString &newShortName)
97{
98 if (newShortName == m_shortName) {
99 return;
100 }
101
102 m_shortName = newShortName;
103 Q_EMIT shortNameChanged();
104}
105
106LineChart::LineChart(QQuickItem *parent)
107 : XYChart(parent)
108{
109}
110
111bool LineChart::interpolate() const
112{
113 return m_interpolate;
114}
115
116qreal LineChart::lineWidth() const
117{
118 return m_lineWidth;
119}
120
121qreal LineChart::fillOpacity() const
122{
123 return m_fillOpacity;
124}
125
126void LineChart::setInterpolate(bool newInterpolate)
127{
128 if (newInterpolate == m_interpolate) {
129 return;
130 }
131
132 m_interpolate = newInterpolate;
133 polish();
134 Q_EMIT interpolateChanged();
135}
136
137void LineChart::setLineWidth(qreal width)
138{
139 if (qFuzzyCompare(p1: m_lineWidth, p2: width)) {
140 return;
141 }
142
143 m_lineWidth = width;
144 update();
145 Q_EMIT lineWidthChanged();
146}
147
148void LineChart::setFillOpacity(qreal opacity)
149{
150 if (qFuzzyCompare(p1: m_fillOpacity, p2: opacity)) {
151 return;
152 }
153
154 m_fillOpacity = opacity;
155 update();
156 Q_EMIT fillOpacityChanged();
157}
158
159ChartDataSource *LineChart::fillColorSource() const
160{
161 return m_fillColorSource;
162}
163
164void LineChart::setFillColorSource(ChartDataSource *newFillColorSource)
165{
166 if (newFillColorSource == m_fillColorSource) {
167 return;
168 }
169
170 m_fillColorSource = newFillColorSource;
171 update();
172 Q_EMIT fillColorSourceChanged();
173}
174
175QQmlComponent *LineChart::pointDelegate() const
176{
177 return m_pointDelegate;
178}
179
180void LineChart::setPointDelegate(QQmlComponent *newPointDelegate)
181{
182 if (newPointDelegate == m_pointDelegate) {
183 return;
184 }
185
186 m_pointDelegate = newPointDelegate;
187 for (auto entry : std::as_const(t&: m_pointDelegates)) {
188 qDeleteAll(c: entry);
189 }
190 m_pointDelegates.clear();
191 polish();
192 Q_EMIT pointDelegateChanged();
193}
194
195void LineChart::updatePolish()
196{
197 if (m_rangeInvalid) {
198 updateComputedRange();
199 m_rangeInvalid = false;
200 }
201
202 QList<QVector2D> previousValues;
203
204 const auto range = computedRange();
205 const auto sources = valueSources();
206 for (int i = 0; i < sources.size(); ++i) {
207 auto valueSource = sources.at(i);
208
209 float stepSize = width() / (range.distanceX - 1);
210 QList<QVector2D> values(range.distanceX);
211 auto generator = [&, i = range.startX]() mutable -> QVector2D {
212 float value = 0;
213 if (range.distanceY != 0) {
214 value = (valueSource->item(index: i).toFloat() - range.startY) / range.distanceY;
215 }
216
217 auto result = QVector2D{direction() == Direction::ZeroAtStart ? i * stepSize : float(boundingRect().right()) - i * stepSize, value};
218 i++;
219 return result;
220 };
221
222 if (direction() == Direction::ZeroAtStart) {
223 std::generate_n(first: values.begin(), n: range.distanceX, gen: generator);
224 } else {
225 std::generate_n(first: values.rbegin(), n: range.distanceX, gen: generator);
226 }
227
228 if (stacked() && !previousValues.isEmpty()) {
229 if (values.size() != previousValues.size()) {
230 qWarning() << "Value source" << valueSource->objectName()
231 << "has a different number of elements from the previous source. Ignoring stacking for this source.";
232 } else {
233 std::for_each(first: values.begin(), last: values.end(), f: [previousValues, i = 0](QVector2D &point) mutable {
234 point.setY(point.y() + previousValues.at(i: i++).y());
235 });
236 }
237 }
238 previousValues = values;
239
240 if (m_pointDelegate) {
241 auto &delegates = m_pointDelegates[valueSource];
242 if (delegates.size() != values.size()) {
243 qDeleteAll(c: delegates);
244 createPointDelegates(values, sourceIndex: i);
245 } else {
246 for (int item = 0; item < values.size(); ++item) {
247 auto delegate = delegates.at(i: item);
248 updatePointDelegate(delegate, position: values.at(i: item), value: valueSource->item(index: item), sourceIndex: i);
249 }
250 }
251 }
252
253 if (m_interpolate) {
254 m_values[valueSource] = interpolatePoints(points: values, height: height());
255 } else {
256 m_values[valueSource] = values;
257 }
258 }
259
260 const auto pointKeys = m_pointDelegates.keys();
261 for (auto key : pointKeys) {
262 if (!sources.contains(t: key)) {
263 qDeleteAll(c: m_pointDelegates[key]);
264 m_pointDelegates.remove(key);
265 }
266 }
267
268 update();
269}
270
271QSGNode *LineChart::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data)
272{
273 Q_UNUSED(data);
274
275 if (!node) {
276 node = new QSGNode();
277 }
278
279 const auto sources = valueSources();
280 for (int i = 0; i < sources.size(); ++i) {
281 int childIndex = sources.size() - 1 - i;
282 while (childIndex >= node->childCount()) {
283 node->appendChildNode(node: new LineChartNode{});
284 }
285 auto lineNode = static_cast<LineChartNode *>(node->childAtIndex(i: childIndex));
286 auto color = colorSource() ? colorSource()->item(index: i).value<QColor>() : Qt::black;
287 auto fillColor = m_fillColorSource ? m_fillColorSource->item(index: i).value<QColor>() : colorWithAlpha(color, opacity: m_fillOpacity);
288 updateLineNode(node: lineNode, lineColor: color, fillColor, valueSource: sources.at(i));
289 }
290
291 while (node->childCount() > sources.size()) {
292 // removeChildNode unfortunately does not take care of deletion so we
293 // need to handle this manually.
294 auto lastNode = node->childAtIndex(i: node->childCount() - 1);
295 node->removeChildNode(node: lastNode);
296 delete lastNode;
297 }
298
299 return node;
300}
301
302void LineChart::onDataChanged()
303{
304 m_rangeInvalid = true;
305 polish();
306}
307
308void LineChart::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
309{
310 XYChart::geometryChange(newGeometry, oldGeometry);
311 if (newGeometry != oldGeometry) {
312 polish();
313 }
314}
315
316void LineChart::updateLineNode(LineChartNode *node, const QColor &lineColor, const QColor &fillColor, ChartDataSource *valueSource)
317{
318 if (window()) {
319 node->setRect(rect: boundingRect(), devicePixelRatio: window()->devicePixelRatio());
320 } else {
321 node->setRect(rect: boundingRect(), devicePixelRatio: 1.0);
322 }
323 node->setLineColor(lineColor);
324 node->setFillColor(fillColor);
325 node->setLineWidth(m_lineWidth);
326
327 auto values = m_values.value(key: valueSource);
328 node->setValues(values);
329
330 node->updatePoints();
331}
332
333void LineChart::createPointDelegates(const QList<QVector2D> &values, int sourceIndex)
334{
335 auto valueSource = valueSources().at(i: sourceIndex);
336
337 QList<QQuickItem *> delegates;
338 for (int i = 0; i < values.size(); ++i) {
339 auto delegate = qobject_cast<QQuickItem *>(o: m_pointDelegate->beginCreate(qmlContext(m_pointDelegate)));
340 if (!delegate) {
341 qWarning() << "Delegate creation for point" << i << "of value source" << valueSource->objectName()
342 << "failed, make sure pointDelegate is a QQuickItem";
343 delegate = new QQuickItem(this);
344 }
345
346 delegate->setParent(this);
347 delegate->setParentItem(this);
348 updatePointDelegate(delegate, position: values.at(i), value: valueSource->item(index: i), sourceIndex);
349
350 m_pointDelegate->completeCreate();
351
352 delegates.append(t: delegate);
353 }
354
355 m_pointDelegates.insert(key: valueSource, value: delegates);
356}
357
358void LineChart::updatePointDelegate(QQuickItem *delegate, const QVector2D &position, const QVariant &value, int sourceIndex)
359{
360 auto pos = QPointF{position.x() - delegate->width() / 2, (1.0 - position.y()) * height() - delegate->height() / 2};
361 delegate->setPosition(pos);
362
363 auto attached = static_cast<LineChartAttached *>(qmlAttachedPropertiesObject<LineChart>(obj: delegate, create: true));
364 attached->setValue(value);
365 attached->setColor(colorSource() ? colorSource()->item(index: sourceIndex).value<QColor>() : Qt::black);
366 attached->setName(nameSource() ? nameSource()->item(index: sourceIndex).toString() : QString{});
367 attached->setShortName(shortNameSource() ? shortNameSource()->item(index: sourceIndex).toString() : QString{});
368}
369
370// Smoothly interpolate between points, using monotonic cubic interpolation.
371QList<QVector2D> interpolatePoints(const QList<QVector2D> &points, float height)
372{
373 if (points.size() < 2) {
374 return points;
375 }
376
377 auto tangents = calculateTangents(points, height);
378
379 QList<QVector2D> result;
380
381 auto current = QVector2D{0.0, points.first().y() * height};
382 result.append(t: QVector2D{0.0, points.first().y()});
383
384 for (int i = 0; i < points.size() - 1; ++i) {
385 auto next = QVector2D{points.at(i: i + 1).x(), points.at(i: i + 1).y() * height};
386
387 auto currentTangent = tangents.at(i);
388 auto nextTangent = tangents.at(i: i + 1);
389
390 auto stepCount = int(std::max(a: 1.0f, b: (next.x() - current.x()) / PixelsPerStep));
391 auto stepSize = (next.x() - current.x()) / stepCount;
392
393 if (stepCount == 1 || qFuzzyIsNull(f: next.y() - current.y())) {
394 result.append(t: QVector2D{next.x(), next.y() / height});
395 current = next;
396 continue;
397 }
398
399 for (auto delta = current.x(); delta < next.x(); delta += stepSize) {
400 auto interpolated = cubicHermite(first: current, second: next, step: delta, mFirst: currentTangent, mSecond: nextTangent);
401 interpolated.setY(interpolated.y() / height);
402 result.append(t: interpolated);
403 }
404
405 current = next;
406 }
407
408 current.setY(current.y() / height);
409 result.append(t: current);
410
411 return result;
412}
413
414// This calculates the tangents for monotonic cubic spline interpolation.
415// See https://en.wikipedia.org/wiki/Monotone_cubic_interpolation for details.
416QList<float> calculateTangents(const QList<QVector2D> &points, float height)
417{
418 QList<float> secantSlopes;
419 secantSlopes.reserve(asize: points.size());
420
421 QList<float> tangents;
422 tangents.reserve(asize: points.size());
423
424 float previousSlope = 0.0;
425 float slope = 0.0;
426
427 for (int i = 0; i < points.size() - 1; ++i) {
428 auto current = points.at(i);
429 auto next = points.at(i: i + 1);
430
431 previousSlope = slope;
432 slope = (next.y() * height - current.y() * height) / (next.x() - current.x());
433
434 secantSlopes.append(t: slope);
435
436 if (i == 0) {
437 tangents.append(t: slope);
438 } else if (previousSlope * slope < 0.0) {
439 tangents.append(t: 0.0);
440 } else {
441 tangents.append(t: (previousSlope + slope) / 2.0);
442 }
443 }
444 tangents.append(t: secantSlopes.last());
445
446 for (int i = 0; i < points.size() - 1; ++i) {
447 auto slope = secantSlopes.at(i);
448
449 if (qFuzzyIsNull(f: slope)) {
450 tangents[i] = 0.0;
451 tangents[i + 1] = 0.0;
452 continue;
453 }
454
455 auto alpha = tangents.at(i) / slope;
456 auto beta = tangents.at(i: i + 1) / slope;
457
458 if (alpha < 0.0) {
459 tangents[i] = 0.0;
460 }
461
462 if (beta < 0.0) {
463 tangents[i + 1] = 0.0;
464 }
465
466 auto length = alpha * alpha + beta * beta;
467 if (length > 9) {
468 auto tau = 3.0 / sqrt(x: length);
469 tangents[i] = tau * alpha * slope;
470 tangents[i + 1] = tau * beta * slope;
471 }
472 }
473
474 return tangents;
475}
476
477// Cubic Hermite Interpolation between two points
478// Given two points, an X value between those two points and two tangents, this
479// will perform cubic hermite interpolation between the two points.
480// See https://en.wikipedia.org/wiki/Cubic_Hermite_spline for details as well as
481// the above mentioned article on monotonic interpolation.
482QVector2D cubicHermite(const QVector2D &first, const QVector2D &second, float step, float mFirst, float mSecond)
483{
484 const auto delta = second.x() - first.x();
485 const auto t = (step - first.x()) / delta;
486
487 // Hermite basis values
488 // h₀₀(t) = 2t³ - 3t² + 1
489 const auto h00 = 2.0f * std::pow(x: t, y: 3.0f) - 3.0f * std::pow(x: t, y: 2.0f) + 1.0f;
490 // h₁₀(t) = t³ - 2t² + t
491 const auto h10 = std::pow(x: t, y: 3.0f) - 2.0f * std::pow(x: t, y: 2.0f) + t;
492 // h₀₁(t) = -2t³ + 3t²
493 const auto h01 = -2.0f * std::pow(x: t, y: 3.0f) + 3.0f * std::pow(x: t, y: 2.0f);
494 // h₁₁(t) = t³ - t²
495 const auto h11 = std::pow(x: t, y: 3.0f) - std::pow(x: t, y: 2.0f);
496
497 auto result = QVector2D{step, first.y() * h00 + delta * mFirst * h10 + second.y() * h01 + delta * mSecond * h11};
498 return result;
499}
500
501#include "moc_LineChart.cpp"
502

source code of kquickcharts/src/LineChart.cpp