1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Charts module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 or (at your option) any later version
20** approved by the KDE Free Qt Foundation. The licenses are as published by
21** the Free Software Foundation and appearing in the file LICENSE.GPL3
22** included in the packaging of this file. Please review the following
23** information to ensure the GNU General Public License requirements will
24** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25**
26** $QT_END_LICENSE$
27**
28****************************************************************************/
29
30#include <private/splinechartitem_p.h>
31#include <private/qsplineseries_p.h>
32#include <private/chartpresenter_p.h>
33#include <private/splineanimation_p.h>
34#include <private/polardomain_p.h>
35#include <QtGui/QPainter>
36#include <QtWidgets/QGraphicsSceneMouseEvent>
37
38QT_CHARTS_BEGIN_NAMESPACE
39
40SplineChartItem::SplineChartItem(QSplineSeries *series, QGraphicsItem *item)
41 : XYChart(series,item),
42 m_series(series),
43 m_pointsVisible(false),
44 m_animation(0),
45 m_pointLabelsVisible(false),
46 m_pointLabelsFormat(series->pointLabelsFormat()),
47 m_pointLabelsFont(series->pointLabelsFont()),
48 m_pointLabelsColor(series->pointLabelsColor()),
49 m_pointLabelsClipping(true),
50 m_mousePressed(false)
51{
52 setAcceptHoverEvents(true);
53 setFlag(flag: QGraphicsItem::ItemIsSelectable);
54 setZValue(ChartPresenter::SplineChartZValue);
55 QObject::connect(sender: m_series->d_func(), SIGNAL(updated()), receiver: this, SLOT(handleUpdated()));
56 QObject::connect(sender: series, SIGNAL(visibleChanged()), receiver: this, SLOT(handleUpdated()));
57 QObject::connect(sender: series, SIGNAL(opacityChanged()), receiver: this, SLOT(handleUpdated()));
58 QObject::connect(sender: series, SIGNAL(pointLabelsFormatChanged(QString)),
59 receiver: this, SLOT(handleUpdated()));
60 QObject::connect(sender: series, SIGNAL(pointLabelsVisibilityChanged(bool)),
61 receiver: this, SLOT(handleUpdated()));
62 QObject::connect(sender: series, SIGNAL(pointLabelsFontChanged(QFont)), receiver: this, SLOT(handleUpdated()));
63 QObject::connect(sender: series, SIGNAL(pointLabelsColorChanged(QColor)), receiver: this, SLOT(handleUpdated()));
64 QObject::connect(sender: series, SIGNAL(pointLabelsClippingChanged(bool)), receiver: this, SLOT(handleUpdated()));
65 handleUpdated();
66}
67
68QRectF SplineChartItem::boundingRect() const
69{
70 return m_rect;
71}
72
73QPainterPath SplineChartItem::shape() const
74{
75 return m_fullPath;
76}
77
78void SplineChartItem::setAnimation(SplineAnimation *animation)
79{
80 m_animation = animation;
81 XYChart::setAnimation(animation);
82}
83
84ChartAnimation *SplineChartItem::animation() const
85{
86 return m_animation;
87}
88
89void SplineChartItem::setControlGeometryPoints(QVector<QPointF>& points)
90{
91 m_controlPoints = points;
92}
93
94QVector<QPointF> SplineChartItem::controlGeometryPoints() const
95{
96 return m_controlPoints;
97}
98
99void SplineChartItem::updateChart(QVector<QPointF> &oldPoints, QVector<QPointF> &newPoints, int index)
100{
101 QVector<QPointF> controlPoints;
102 if (newPoints.count() >= 2)
103 controlPoints = calculateControlPoints(points: newPoints);
104
105 if (m_animation)
106 m_animation->setup(oldPoints, newPoints, oldContorlPoints&: m_controlPoints, newControlPoints&: controlPoints, index);
107
108 m_points = newPoints;
109 m_controlPoints = controlPoints;
110 setDirty(false);
111
112 if (m_animation)
113 presenter()->startAnimation(animation: m_animation);
114 else
115 updateGeometry();
116}
117
118void SplineChartItem::updateGeometry()
119{
120 const QVector<QPointF> &points = m_points;
121 const QVector<QPointF> &controlPoints = m_controlPoints;
122
123 if ((points.size() < 2) || (controlPoints.size() < 2)) {
124 prepareGeometryChange();
125 m_path = QPainterPath();
126 m_rect = QRect();
127 return;
128 }
129
130 Q_ASSERT(points.count() * 2 - 2 == controlPoints.count());
131
132 QPainterPath splinePath;
133 QPainterPath fullPath;
134 // Use worst case scenario to determine required margin.
135 qreal margin = m_linePen.width() * 1.42;
136
137 if (m_series->chart()->chartType() == QChart::ChartTypePolar) {
138 QPainterPath splinePathLeft;
139 QPainterPath splinePathRight;
140 QPainterPath *currentSegmentPath = 0;
141 QPainterPath *previousSegmentPath = 0;
142 qreal minX = domain()->minX();
143 qreal maxX = domain()->maxX();
144 qreal minY = domain()->minY();
145 QPointF currentSeriesPoint = m_series->at(index: 0);
146 QPointF currentGeometryPoint = points.at(i: 0);
147 QPointF previousGeometryPoint = points.at(i: 0);
148 bool pointOffGrid = false;
149 bool previousPointWasOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX);
150 m_visiblePoints.clear();
151 m_visiblePoints.reserve(asize: points.size());
152
153 qreal domainRadius = domain()->size().height() / 2.0;
154 const QPointF centerPoint(domainRadius, domainRadius);
155
156 if (!previousPointWasOffGrid) {
157 fullPath.moveTo(p: points.at(i: 0));
158 // Do not draw points for points below minimum Y.
159 if (m_pointsVisible && currentSeriesPoint.y() >= minY)
160 m_visiblePoints.append(t: currentGeometryPoint);
161 }
162
163 qreal leftMarginLine = centerPoint.x() - margin;
164 qreal rightMarginLine = centerPoint.x() + margin;
165 qreal horizontal = centerPoint.y();
166
167 // See ScatterChartItem::updateGeometry() for explanation why seriesLastIndex is needed
168 const int seriesLastIndex = m_series->count() - 1;
169
170 for (int i = 1; i < points.size(); i++) {
171 // Interpolating spline fragments accurately is not trivial, and would anyway be ugly
172 // when thick pen is used, so we work around it by utilizing three separate
173 // paths for spline segments and clip those with custom regions at paint time.
174 // "Right" path contains segments that cross the axis line with visible point on the
175 // right side of the axis line, as well as segments that have one point within the margin
176 // on the right side of the axis line and another point on the right side of the chart.
177 // "Left" path contains points with similarly on the left side.
178 // "Full" path contains rest of the points.
179 // This doesn't yield perfect results always. E.g. when segment covers more than 90
180 // degrees and both of the points are within the margin, one in the top half and one in the
181 // bottom half of the chart, the bottom one gets clipped incorrectly.
182 // However, this should be rare occurrence in any sensible chart.
183 currentSeriesPoint = m_series->at(index: qMin(a: seriesLastIndex, b: i));
184 currentGeometryPoint = points.at(i);
185 pointOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX);
186
187 // Draw something unless both off-grid
188 if (!pointOffGrid || !previousPointWasOffGrid) {
189 bool dummyOk; // We know points are ok, but this is needed
190 qreal currentAngle = static_cast<PolarDomain *>(domain())->toAngularCoordinate(value: currentSeriesPoint.x(), ok&: dummyOk);
191 qreal previousAngle = static_cast<PolarDomain *>(domain())->toAngularCoordinate(value: m_series->at(index: i - 1).x(), ok&: dummyOk);
192
193 if ((qAbs(t: currentAngle - previousAngle) > 180.0)) {
194 // If the angle between two points is over 180 degrees (half X range),
195 // any direct segment between them becomes meaningless.
196 // In this case two line segments are drawn instead, from previous
197 // point to the center and from center to current point.
198 if ((previousAngle < 0.0 || (previousAngle <= 180.0 && previousGeometryPoint.x() < rightMarginLine))
199 && previousGeometryPoint.y() < horizontal) {
200 currentSegmentPath = &splinePathRight;
201 } else if ((previousAngle > 360.0 || (previousAngle > 180.0 && previousGeometryPoint.x() > leftMarginLine))
202 && previousGeometryPoint.y() < horizontal) {
203 currentSegmentPath = &splinePathLeft;
204 } else if (previousAngle > 0.0 && previousAngle < 360.0) {
205 currentSegmentPath = &splinePath;
206 } else {
207 currentSegmentPath = 0;
208 }
209
210 if (currentSegmentPath) {
211 if (previousSegmentPath != currentSegmentPath)
212 currentSegmentPath->moveTo(p: previousGeometryPoint);
213 if (!previousSegmentPath)
214 fullPath.moveTo(p: previousGeometryPoint);
215
216 currentSegmentPath->lineTo(p: centerPoint);
217 fullPath.lineTo(p: centerPoint);
218 }
219
220 previousSegmentPath = currentSegmentPath;
221
222 if ((currentAngle < 0.0 || (currentAngle <= 180.0 && currentGeometryPoint.x() < rightMarginLine))
223 && currentGeometryPoint.y() < horizontal) {
224 currentSegmentPath = &splinePathRight;
225 } else if ((currentAngle > 360.0 || (currentAngle > 180.0 &&currentGeometryPoint.x() > leftMarginLine))
226 && currentGeometryPoint.y() < horizontal) {
227 currentSegmentPath = &splinePathLeft;
228 } else if (currentAngle > 0.0 && currentAngle < 360.0) {
229 currentSegmentPath = &splinePath;
230 } else {
231 currentSegmentPath = 0;
232 }
233
234 if (currentSegmentPath) {
235 if (previousSegmentPath != currentSegmentPath)
236 currentSegmentPath->moveTo(p: centerPoint);
237 if (!previousSegmentPath)
238 fullPath.moveTo(p: centerPoint);
239
240 currentSegmentPath->lineTo(p: currentGeometryPoint);
241 fullPath.lineTo(p: currentGeometryPoint);
242 }
243 } else {
244 QPointF cp1 = controlPoints[2 * (i - 1)];
245 QPointF cp2 = controlPoints[(2 * i) - 1];
246
247 if (previousAngle < 0.0 || currentAngle < 0.0
248 || ((previousAngle <= 180.0 && currentAngle <= 180.0)
249 && ((previousGeometryPoint.x() < rightMarginLine && previousGeometryPoint.y() < horizontal)
250 || (currentGeometryPoint.x() < rightMarginLine && currentGeometryPoint.y() < horizontal)))) {
251 currentSegmentPath = &splinePathRight;
252 } else if (previousAngle > 360.0 || currentAngle > 360.0
253 || ((previousAngle > 180.0 && currentAngle > 180.0)
254 && ((previousGeometryPoint.x() > leftMarginLine && previousGeometryPoint.y() < horizontal)
255 || (currentGeometryPoint.x() > leftMarginLine && currentGeometryPoint.y() < horizontal)))) {
256 currentSegmentPath = &splinePathLeft;
257 } else {
258 currentSegmentPath = &splinePath;
259 }
260
261 if (currentSegmentPath != previousSegmentPath)
262 currentSegmentPath->moveTo(p: previousGeometryPoint);
263 if (!previousSegmentPath)
264 fullPath.moveTo(p: previousGeometryPoint);
265
266 fullPath.cubicTo(ctrlPt1: cp1, ctrlPt2: cp2, endPt: currentGeometryPoint);
267 currentSegmentPath->cubicTo(ctrlPt1: cp1, ctrlPt2: cp2, endPt: currentGeometryPoint);
268 }
269 } else {
270 currentSegmentPath = 0;
271 }
272
273 previousPointWasOffGrid = pointOffGrid;
274 if (!pointOffGrid && m_pointsVisible && currentSeriesPoint.y() >= minY)
275 m_visiblePoints.append(t: currentGeometryPoint);
276 previousSegmentPath = currentSegmentPath;
277 previousGeometryPoint = currentGeometryPoint;
278 }
279
280 m_pathPolarRight = splinePathRight;
281 m_pathPolarLeft = splinePathLeft;
282 // Note: This construction of m_fullpath is not perfect. The partial segments that are
283 // outside left/right clip regions at axis boundary still generate hover/click events,
284 // because shape doesn't get clipped. It doesn't seem possible to do sensibly.
285 } else { // not polar
286 splinePath.moveTo(p: points.at(i: 0));
287 for (int i = 0; i < points.size() - 1; i++) {
288 const QPointF &point = points.at(i: i + 1);
289 splinePath.cubicTo(ctrlPt1: controlPoints[2 * i], ctrlPt2: controlPoints[2 * i + 1], endPt: point);
290 }
291 fullPath = splinePath;
292 }
293
294 QPainterPathStroker stroker;
295 // The full path is comprised of three separate paths.
296 // This is why we are prepared for the "worst case" scenario, i.e. use always MiterJoin and
297 // multiply line width with square root of two when defining shape and bounding rectangle.
298 stroker.setWidth(margin);
299 stroker.setJoinStyle(Qt::MiterJoin);
300 stroker.setCapStyle(Qt::SquareCap);
301 stroker.setMiterLimit(m_linePen.miterLimit());
302
303 // Only zoom in if the bounding rects of the path fit inside int limits. QWidget::update() uses
304 // a region that has to be compatible with QRect.
305 QPainterPath checkShapePath = stroker.createStroke(path: fullPath);
306 if (checkShapePath.boundingRect().height() <= INT_MAX
307 && checkShapePath.boundingRect().width() <= INT_MAX
308 && splinePath.boundingRect().height() <= INT_MAX
309 && splinePath.boundingRect().width() <= INT_MAX) {
310 m_path = splinePath;
311
312 prepareGeometryChange();
313
314 m_fullPath = checkShapePath;
315 m_rect = m_fullPath.boundingRect();
316 }
317}
318
319/*!
320 Calculates control points which are needed by QPainterPath.cubicTo function to draw the cubic Bezier cureve between two points.
321 */
322QVector<QPointF> SplineChartItem::calculateControlPoints(const QVector<QPointF> &points)
323{
324 QVector<QPointF> controlPoints;
325 controlPoints.resize(asize: points.count() * 2 - 2);
326
327 int n = points.count() - 1;
328
329 if (n == 1) {
330 //for n==1
331 controlPoints[0].setX((2 * points[0].x() + points[1].x()) / 3);
332 controlPoints[0].setY((2 * points[0].y() + points[1].y()) / 3);
333 controlPoints[1].setX(2 * controlPoints[0].x() - points[0].x());
334 controlPoints[1].setY(2 * controlPoints[0].y() - points[0].y());
335 return controlPoints;
336 }
337
338 // Calculate first Bezier control points
339 // Set of equations for P0 to Pn points.
340 //
341 // | 2 1 0 0 ... 0 0 0 ... 0 0 0 | | P1_1 | | P0 + 2 * P1 |
342 // | 1 4 1 0 ... 0 0 0 ... 0 0 0 | | P1_2 | | 4 * P1 + 2 * P2 |
343 // | 0 1 4 1 ... 0 0 0 ... 0 0 0 | | P1_3 | | 4 * P2 + 2 * P3 |
344 // | . . . . . . . . . . . . | | ... | | ... |
345 // | 0 0 0 0 ... 1 4 1 ... 0 0 0 | * | P1_i | = | 4 * P(i-1) + 2 * Pi |
346 // | . . . . . . . . . . . . | | ... | | ... |
347 // | 0 0 0 0 0 0 0 0 ... 1 4 1 | | P1_(n-1)| | 4 * P(n-2) + 2 * P(n-1) |
348 // | 0 0 0 0 0 0 0 0 ... 0 2 7 | | P1_n | | 8 * P(n-1) + Pn |
349 //
350 QVector<qreal> vector;
351 vector.resize(asize: n);
352
353 vector[0] = points[0].x() + 2 * points[1].x();
354
355
356 for (int i = 1; i < n - 1; ++i)
357 vector[i] = 4 * points[i].x() + 2 * points[i + 1].x();
358
359 vector[n - 1] = (8 * points[n - 1].x() + points[n].x()) / 2.0;
360
361 QVector<qreal> xControl = firstControlPoints(vector);
362
363 vector[0] = points[0].y() + 2 * points[1].y();
364
365 for (int i = 1; i < n - 1; ++i)
366 vector[i] = 4 * points[i].y() + 2 * points[i + 1].y();
367
368 vector[n - 1] = (8 * points[n - 1].y() + points[n].y()) / 2.0;
369
370 QVector<qreal> yControl = firstControlPoints(vector);
371
372 for (int i = 0, j = 0; i < n; ++i, ++j) {
373
374 controlPoints[j].setX(xControl[i]);
375 controlPoints[j].setY(yControl[i]);
376
377 j++;
378
379 if (i < n - 1) {
380 controlPoints[j].setX(2 * points[i + 1].x() - xControl[i + 1]);
381 controlPoints[j].setY(2 * points[i + 1].y() - yControl[i + 1]);
382 } else {
383 controlPoints[j].setX((points[n].x() + xControl[n - 1]) / 2);
384 controlPoints[j].setY((points[n].y() + yControl[n - 1]) / 2);
385 }
386 }
387 return controlPoints;
388}
389
390QVector<qreal> SplineChartItem::firstControlPoints(const QVector<qreal>& vector)
391{
392 QVector<qreal> result;
393
394 int count = vector.count();
395 result.resize(asize: count);
396 result[0] = vector[0] / 2.0;
397
398 QVector<qreal> temp;
399 temp.resize(asize: count);
400 temp[0] = 0;
401
402 qreal b = 2.0;
403
404 for (int i = 1; i < count; i++) {
405 temp[i] = 1 / b;
406 b = (i < count - 1 ? 4.0 : 3.5) - temp[i];
407 result[i] = (vector[i] - result[i - 1]) / b;
408 }
409
410 for (int i = 1; i < count; i++)
411 result[count - i - 1] -= temp[count - i] * result[count - i];
412
413 return result;
414}
415
416//handlers
417
418void SplineChartItem::handleUpdated()
419{
420 setVisible(m_series->isVisible());
421 setOpacity(m_series->opacity());
422 m_pointsVisible = m_series->pointsVisible();
423 m_linePen = m_series->pen();
424 m_pointPen = m_series->pen();
425 m_pointPen.setWidthF(2 * m_pointPen.width());
426 m_pointLabelsFormat = m_series->pointLabelsFormat();
427 m_pointLabelsVisible = m_series->pointLabelsVisible();
428 m_pointLabelsFont = m_series->pointLabelsFont();
429 m_pointLabelsColor = m_series->pointLabelsColor();
430 bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping();
431 m_pointLabelsClipping = m_series->pointLabelsClipping();
432 // Update whole chart in case label clipping changed as labels can be outside series area
433 if (labelClippingChanged)
434 m_series->chart()->update();
435 else
436 update();
437}
438
439//painter
440
441void SplineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
442{
443 Q_UNUSED(widget)
444 Q_UNUSED(option)
445
446 QRectF clipRect = QRectF(QPointF(0, 0), domain()->size());
447
448 painter->save();
449 painter->setPen(m_linePen);
450 painter->setBrush(Qt::NoBrush);
451
452 if (m_series->chart()->chartType() == QChart::ChartTypePolar) {
453 qreal halfWidth = domain()->size().width() / 2.0;
454 QRectF clipRectLeft = QRectF(0, 0, halfWidth, domain()->size().height());
455 QRectF clipRectRight = QRectF(halfWidth, 0, halfWidth, domain()->size().height());
456 QRegion fullPolarClipRegion(clipRect.toRect(), QRegion::Ellipse);
457 QRegion clipRegionLeft(fullPolarClipRegion.intersected(r: clipRectLeft.toRect()));
458 QRegion clipRegionRight(fullPolarClipRegion.intersected(r: clipRectRight.toRect()));
459 painter->setClipRegion(clipRegionLeft);
460 painter->drawPath(path: m_pathPolarLeft);
461 painter->setClipRegion(clipRegionRight);
462 painter->drawPath(path: m_pathPolarRight);
463 painter->setClipRegion(fullPolarClipRegion);
464 } else {
465 painter->setClipRect(clipRect);
466 }
467
468 painter->drawPath(path: m_path);
469
470 if (m_pointsVisible) {
471 painter->setPen(m_pointPen);
472 if (m_series->chart()->chartType() == QChart::ChartTypePolar)
473 painter->drawPoints(points: m_visiblePoints);
474 else
475 painter->drawPoints(points: geometryPoints());
476 }
477
478 if (m_pointLabelsVisible) {
479 if (m_pointLabelsClipping)
480 painter->setClipping(true);
481 else
482 painter->setClipping(false);
483 m_series->d_func()->drawSeriesPointLabels(painter, points: m_points, offset: m_linePen.width() / 2);
484 }
485
486 painter->restore();
487}
488
489void SplineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
490{
491 emit XYChart::pressed(point: domain()->calculateDomainPoint(point: event->pos()));
492 m_lastMousePos = event->pos();
493 m_mousePressed = true;
494 QGraphicsItem::mousePressEvent(event);
495}
496
497void SplineChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
498{
499 emit XYChart::hovered(point: domain()->calculateDomainPoint(point: event->pos()), state: true);
500 QGraphicsItem::hoverEnterEvent(event);
501}
502
503void SplineChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
504{
505 emit XYChart::hovered(point: domain()->calculateDomainPoint(point: event->pos()), state: false);
506 QGraphicsItem::hoverLeaveEvent(event);
507}
508
509void SplineChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
510{
511 emit XYChart::released(point: domain()->calculateDomainPoint(point: m_lastMousePos));
512 if (m_mousePressed)
513 emit XYChart::clicked(point: domain()->calculateDomainPoint(point: m_lastMousePos));
514 m_mousePressed = false;
515 QGraphicsItem::mouseReleaseEvent(event);
516}
517
518void SplineChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
519{
520 emit XYChart::doubleClicked(point: domain()->calculateDomainPoint(point: m_lastMousePos));
521 QGraphicsItem::mouseDoubleClickEvent(event);
522}
523
524QT_CHARTS_END_NAMESPACE
525
526#include "moc_splinechartitem_p.cpp"
527

source code of qtcharts/src/charts/splinechart/splinechartitem.cpp