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 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(new LineChartNode{});
284 }
285 auto lineNode = static_cast<LineChartNode *>(node->childAtIndex(childIndex));
286 auto color = colorSource() ? colorSource()->item(i).value<QColor>() : Qt::black;
287 auto fillColor = m_fillColorSource ? m_fillColorSource->item(i).value<QColor>() : colorWithAlpha(color, m_fillOpacity);
288 updateLineNode(lineNode, color, fillColor, 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(node->childCount() - 1);
295 node->removeChildNode(lastNode);
296 delete lastNode;
297 }
298
299 return node;
300}
301
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(boundingRect(), window()->devicePixelRatio());
320 } else {
321 node->setRect(boundingRect(), 1.0);
322 }
323 node->setLineColor(lineColor);
324 node->setFillColor(fillColor);
325 node->setLineWidth(m_lineWidth);
326
327 auto values = m_values.value(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(sourceIndex);
336
337 QList<QQuickItem *> delegates;
338 for (int i = 0; i < values.size(); ++i) {
339 auto delegate = qobject_cast<QQuickItem *>(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, values.at(i), valueSource->item(i), sourceIndex);
349
350 m_pointDelegate->completeCreate();
351
352 delegates.append(delegate);
353 }
354
355 m_pointDelegates.insert(valueSource, 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>(delegate, true));
364 attached->setValue(value);
365 attached->setColor(colorSource() ? colorSource()->item(sourceIndex).value<QColor>() : Qt::black);
366 attached->setName(nameSource() ? nameSource()->item(sourceIndex).toString() : QString{});
367 attached->setShortName(shortNameSource() ? shortNameSource()->item(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(QVector2D{0.0, points.first().y()});
383
384 for (int i = 0; i < points.size() - 1; ++i) {
385 auto next = QVector2D{points.at(i + 1).x(), points.at(i + 1).y() * height};
386
387 auto currentTangent = tangents.at(i);
388 auto nextTangent = tangents.at(i + 1);
389
390 auto stepCount = int(std::max(1.0f, (next.x() - current.x()) / PixelsPerStep));
391 auto stepSize = (next.x() - current.x()) / stepCount;
392
393 if (stepCount == 1 || qFuzzyIsNull(next.y() - current.y())) {
394 result.append(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(current, next, delta, currentTangent, nextTangent);
401 interpolated.setY(interpolated.y() / height);
402 result.append(interpolated);
403 }
404
405 current = next;
406 }
407
408 current.setY(current.y() / height);
409 result.append(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(points.size());
420
421 QList<float> tangents;
422 tangents.reserve(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 + 1);
430
431 previousSlope = slope;
432 slope = (next.y() * height - current.y() * height) / (next.x() - current.x());
433
434 secantSlopes.append(slope);
435
436 if (i == 0) {
437 tangents.append(slope);
438 } else if (previousSlope * slope < 0.0) {
439 tangents.append(0.0);
440 } else {
441 tangents.append((previousSlope + slope) / 2.0);
442 }
443 }
444 tangents.append(secantSlopes.last());
445
446 for (int i = 0; i < points.size() - 1; ++i) {
447 auto slope = secantSlopes.at(i);
448
449 if (qFuzzyIsNull(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 + 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(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(t, 3.0f) - 3.0f * std::pow(t, 2.0f) + 1.0f;
490 // h₁₀(t) = t³ - 2t² + t
491 const auto h10 = std::pow(t, 3.0f) - 2.0f * std::pow(t, 2.0f) + t;
492 // h₀₁(t) = -2t³ + 3t²
493 const auto h01 = -2.0f * std::pow(t, 3.0f) + 3.0f * std::pow(t, 2.0f);
494 // h₁₁(t) = t³ - t²
495 const auto h11 = std::pow(t, 3.0f) - std::pow(t, 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"
Abstract base class for data sources.
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
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.
char * toString(const EngineQuery &query)
const QList< QKeySequence > & next()
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 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)
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 Tue Mar 26 2024 11:13:57 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.