KQuickCharts

LineChart.cpp
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
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
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
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
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(m_lineWidth, 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(m_fillOpacity, opacity)) {
151 return;
152 }
153
154 m_fillOpacity = opacity;
155 update();
156 Q_EMIT fillOpacityChanged();
157}
158
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
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(m_pointDelegates)) {
188 qDeleteAll(entry);
189 }
190 m_pointDelegates.clear();
191 polish();
192 Q_EMIT pointDelegateChanged();
193}
194
195void LineChart::updatePolish()
196{
197 if (m_rangeInvalid) {
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(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
223 std::generate_n(values.begin(), range.distanceX, generator);
224 } else {
225 std::generate_n(values.rbegin(), range.distanceX, 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(values.begin(), values.end(), [previousValues, i = 0](QVector2D &point) mutable {
234 point.setY(point.y() + previousValues.at(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(delegates);
244 createPointDelegates(values, i);
245 } else {
246 for (int item = 0; item < values.size(); ++item) {
247 auto delegate = delegates.at(item);
248 updatePointDelegate(delegate, values.at(item), valueSource->item(item), i);
249 }
250 }
251 }
252
253 if (m_interpolate) {
254 m_values[valueSource] = interpolatePoints(values, 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(key)) {
263 qDeleteAll(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 highlightIndex = highlight();
280 const auto sources = valueSources();
281 for (int i = 0; i < sources.size(); ++i) {
282 int childIndex = sources.size() - 1 - i;
283 while (childIndex >= node->childCount()) {
284 node->appendChildNode(new LineChartNode{});
285 }
286 auto lineNode = static_cast<LineChartNode *>(node->childAtIndex(childIndex));
287 auto color = colorSource() ? colorSource()->item(i).value<QColor>() : Qt::black;
288 auto fillColor = m_fillColorSource ? m_fillColorSource->item(i).value<QColor>() : colorWithAlpha(color, m_fillOpacity);
289 auto lineWidth = i == highlightIndex ? std::max(m_lineWidth, 3.0) : m_lineWidth;
290
291 if (highlightIndex >= 0 && i != highlightIndex) {
292 color = desaturate(color);
293 fillColor = desaturate(fillColor);
294 }
295
296 updateLineNode(lineNode, sources.at(i), color, fillColor, lineWidth);
297 }
298
299 while (node->childCount() > sources.size()) {
300 // removeChildNode unfortunately does not take care of deletion so we
301 // need to handle this manually.
302 auto lastNode = node->childAtIndex(node->childCount() - 1);
303 node->removeChildNode(lastNode);
304 delete lastNode;
305 }
306
307 if (highlightIndex >= 0) {
308 // Move highlighted node to the end to ensure we always show the
309 // highlighted chart on top. This is done after the above removal to
310 // ensure we don't suddenly remove the highlighted node.
311 auto highlightNode = node->childAtIndex(node->childCount() - 1 - highlightIndex);
312 node->removeChildNode(highlightNode);
313 node->appendChildNode(highlightNode);
314 }
315
316 return node;
317}
318
320{
321 m_rangeInvalid = true;
322 polish();
323}
324
325void LineChart::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
326{
327 XYChart::geometryChange(newGeometry, oldGeometry);
328 if (newGeometry != oldGeometry) {
329 polish();
330 }
331}
332
333void LineChart::updateLineNode(LineChartNode *node, ChartDataSource *valueSource, const QColor &lineColor, const QColor &fillColor, qreal lineWidth)
334{
335 if (window()) {
336 node->setRect(boundingRect(), window()->devicePixelRatio());
337 } else {
338 node->setRect(boundingRect(), 1.0);
339 }
340 node->setLineColor(lineColor);
341 node->setFillColor(fillColor);
342 node->setLineWidth(lineWidth);
343
344 auto values = m_values.value(valueSource);
345 node->setValues(values);
346
347 node->updatePoints();
348}
349
350void LineChart::createPointDelegates(const QList<QVector2D> &values, int sourceIndex)
351{
352 auto valueSource = valueSources().at(sourceIndex);
353
354 QList<QQuickItem *> delegates;
355 for (int i = 0; i < values.size(); ++i) {
356 auto delegate = qobject_cast<QQuickItem *>(m_pointDelegate->beginCreate(qmlContext(m_pointDelegate)));
357 if (!delegate) {
358 qWarning() << "Delegate creation for point" << i << "of value source" << valueSource->objectName()
359 << "failed, make sure pointDelegate is a QQuickItem";
360 delegate = new QQuickItem(this);
361 }
362
363 delegate->setParent(this);
364 delegate->setParentItem(this);
365 updatePointDelegate(delegate, values.at(i), valueSource->item(i), sourceIndex);
366
367 m_pointDelegate->completeCreate();
368
369 delegates.append(delegate);
370 }
371
372 m_pointDelegates.insert(valueSource, delegates);
373}
374
375void LineChart::updatePointDelegate(QQuickItem *delegate, const QVector2D &position, const QVariant &value, int sourceIndex)
376{
377 auto pos = QPointF{position.x() - delegate->width() / 2, (1.0 - position.y()) * height() - delegate->height() / 2};
378 delegate->setPosition(pos);
379
380 auto color = colorSource() ? colorSource()->item(sourceIndex).value<QColor>() : QColor();
381 auto highlightIndex = highlight();
382 if (highlightIndex >= 0) {
383 if (sourceIndex == highlightIndex) {
384 delegate->setZ(1.0);
385 } else {
386 color = desaturate(color);
387 }
388 } else {
389 delegate->setZ(0.0);
390 }
391
392 auto attached = static_cast<LineChartAttached *>(qmlAttachedPropertiesObject<LineChart>(delegate, true));
393 attached->setValue(value);
394 attached->setColor(color);
395 attached->setName(nameSource() ? nameSource()->item(sourceIndex).toString() : QString{});
396 attached->setShortName(shortNameSource() ? shortNameSource()->item(sourceIndex).toString() : QString{});
397}
398
399// Smoothly interpolate between points, using monotonic cubic interpolation.
400QList<QVector2D> interpolatePoints(const QList<QVector2D> &points, float height)
401{
402 if (points.size() < 2) {
403 return points;
404 }
405
406 auto tangents = calculateTangents(points, height);
407
408 QList<QVector2D> result;
409
410 auto current = QVector2D{0.0, points.first().y() * height};
411 result.append(QVector2D{0.0, points.first().y()});
412
413 for (int i = 0; i < points.size() - 1; ++i) {
414 auto next = QVector2D{points.at(i + 1).x(), points.at(i + 1).y() * height};
415
416 auto currentTangent = tangents.at(i);
417 auto nextTangent = tangents.at(i + 1);
418
419 auto stepCount = int(std::max(1.0f, (next.x() - current.x()) / PixelsPerStep));
420 auto stepSize = (next.x() - current.x()) / stepCount;
421
422 if (stepCount == 1 || qFuzzyIsNull(next.y() - current.y())) {
423 result.append(QVector2D{next.x(), next.y() / height});
424 current = next;
425 continue;
426 }
427
428 for (auto delta = current.x(); delta < next.x(); delta += stepSize) {
429 auto interpolated = cubicHermite(current, next, delta, currentTangent, nextTangent);
430 interpolated.setY(interpolated.y() / height);
431 result.append(interpolated);
432 }
433
434 current = next;
435 }
436
437 current.setY(current.y() / height);
438 result.append(current);
439
440 return result;
441}
442
443// This calculates the tangents for monotonic cubic spline interpolation.
444// See https://en.wikipedia.org/wiki/Monotone_cubic_interpolation for details.
445QList<float> calculateTangents(const QList<QVector2D> &points, float height)
446{
447 QList<float> secantSlopes;
448 secantSlopes.reserve(points.size());
449
450 QList<float> tangents;
451 tangents.reserve(points.size());
452
453 float previousSlope = 0.0;
454 float slope = 0.0;
455
456 for (int i = 0; i < points.size() - 1; ++i) {
457 auto current = points.at(i);
458 auto next = points.at(i + 1);
459
460 previousSlope = slope;
461 slope = (next.y() * height - current.y() * height) / (next.x() - current.x());
462
463 secantSlopes.append(slope);
464
465 if (i == 0) {
466 tangents.append(slope);
467 } else if (previousSlope * slope < 0.0) {
468 tangents.append(0.0);
469 } else {
470 tangents.append((previousSlope + slope) / 2.0);
471 }
472 }
473 tangents.append(secantSlopes.last());
474
475 for (int i = 0; i < points.size() - 1; ++i) {
476 auto slope = secantSlopes.at(i);
477
478 if (qFuzzyIsNull(slope)) {
479 tangents[i] = 0.0;
480 tangents[i + 1] = 0.0;
481 continue;
482 }
483
484 auto alpha = tangents.at(i) / slope;
485 auto beta = tangents.at(i + 1) / slope;
486
487 if (alpha < 0.0) {
488 tangents[i] = 0.0;
489 }
490
491 if (beta < 0.0) {
492 tangents[i + 1] = 0.0;
493 }
494
495 auto length = alpha * alpha + beta * beta;
496 if (length > 9) {
497 auto tau = 3.0 / sqrt(length);
498 tangents[i] = tau * alpha * slope;
499 tangents[i + 1] = tau * beta * slope;
500 }
501 }
502
503 return tangents;
504}
505
506// Cubic Hermite Interpolation between two points
507// Given two points, an X value between those two points and two tangents, this
508// will perform cubic hermite interpolation between the two points.
509// See https://en.wikipedia.org/wiki/Cubic_Hermite_spline for details as well as
510// the above mentioned article on monotonic interpolation.
511QVector2D cubicHermite(const QVector2D &first, const QVector2D &second, float step, float mFirst, float mSecond)
512{
513 const auto delta = second.x() - first.x();
514 const auto t = (step - first.x()) / delta;
515
516 // Hermite basis values
517 // h₀₀(t) = 2t³ - 3t² + 1
518 const auto h00 = 2.0f * std::pow(t, 3.0f) - 3.0f * std::pow(t, 2.0f) + 1.0f;
519 // h₁₀(t) = t³ - 2t² + t
520 const auto h10 = std::pow(t, 3.0f) - 2.0f * std::pow(t, 2.0f) + t;
521 // h₀₁(t) = -2t³ + 3t²
522 const auto h01 = -2.0f * std::pow(t, 3.0f) + 3.0f * std::pow(t, 2.0f);
523 // h₁₁(t) = t³ - t²
524 const auto h11 = std::pow(t, 3.0f) - std::pow(t, 2.0f);
525
526 auto result = QVector2D{step, first.y() * h00 + delta * mFirst * h10 + second.y() * h01 + delta * mSecond * h11};
527 return result;
528}
529
530#include "moc_LineChart.cpp"
Abstract base class for data sources.
QColor desaturate(const QColor &input)
Desaturate and de-emphasise a color.
Definition Chart.cpp:179
ChartDataSource * shortNameSource
The data source to use for short names of chart items.
Definition Chart.h:54
QQmlListProperty< ChartDataSource > valueSources
The data sources providing the data this chart needs to render.
Definition Chart.h:70
ChartDataSource * nameSource
The data source to use for names of chart items.
Definition Chart.h:46
ChartDataSource * colorSource
The data source to use for colors of chart items.
Definition Chart.h:62
int highlight
The index of a value source to highlight.
Definition Chart.h:94
An attached property that is exposed to point delegates created in line charts.
Definition LineChart.h:25
QVariant value
The value at the current point.
Definition LineChart.h:35
QString shortName
The short name at the current point.
Definition LineChart.h:59
QColor color
The color at the current point.
Definition LineChart.h:43
QString name
The name at the current point.
Definition LineChart.h:51
ChartDataSource * fillColorSource
A data source that supplies color values for the line charts' fill area.
Definition LineChart.h:119
qreal fillOpacity
The opacity of the area below a line.
Definition LineChart.h:109
QQmlComponent * pointDelegate
A delegate that will be placed at each line chart point.
Definition LineChart.h:135
qreal lineWidth
The width of a line in the chart.
Definition LineChart.h:99
bool interpolate
Interpolate the values in the chart so that the lines become smoothed.
Definition LineChart.h:92
void onDataChanged() override
Called when the data of a value source changes.
A base class for Charts that are based on an X/Y grid.
Definition XYChart.h:33
virtual void updateComputedRange()
Re-calculate the chart's range.
Definition XYChart.cpp:74
Direction direction
Which direction this chart's X axis runs.
Definition XYChart.h:77
bool stacked
Whether the values of each value source should be stacked.
Definition XYChart.h:88
ComputedRange computedRange() const
Get the complete, calculated range for this chart.
Definition XYChart.cpp:69
@ ZeroAtStart
Zero is at the beginning of the chart, values run from begin to end.
QAction * next(const QObject *recvr, const char *slot, QObject *parent)
void setRedF(float red)
void clear()
iterator insert(const Key &key, const T &value)
QList< Key > keys() const const
bool remove(const Key &key)
T value(const Key &key) const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
T & first()
bool isEmpty() const const
T & last()
void reserve(qsizetype size)
qsizetype size() const const
Q_EMITQ_EMIT
virtual QObject * beginCreate(QQmlContext *context)
virtual void completeCreate()
QQuickItem(QQuickItem *parent)
virtual QRectF boundingRect() const const
virtual void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
void polish()
void update()
QQuickWindow * window() const const
void setZ(qreal)
void appendChildNode(QSGNode *node)
QSGNode * childAtIndex(int i) const const
int childCount() const const
void removeChildNode(QSGNode *node)
bool isEmpty() const const
QTextStream & right(QTextStream &stream)
QString toString() const const
T value() const const
float x() const const
float y() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 31 2024 17:21:07 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.