1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <private/splinechartitem_p.h>
5#include <private/qsplineseries_p.h>
6#include <private/chartpresenter_p.h>
7#include <private/splineanimation_p.h>
8#include <private/polardomain_p.h>
9#include <QtGui/QPainter>
10#include <QtWidgets/QGraphicsSceneMouseEvent>
11
12QT_BEGIN_NAMESPACE
13
14SplineChartItem::SplineChartItem(QSplineSeries *series, QGraphicsItem *item)
15 : XYChart(series,item),
16 m_series(series),
17 m_pointsVisible(false),
18 m_animation(0),
19 m_pointLabelsVisible(false),
20 m_markerSize(series->markerSize()),
21 m_pointLabelsFormat(series->pointLabelsFormat()),
22 m_pointLabelsFont(series->pointLabelsFont()),
23 m_pointLabelsColor(series->pointLabelsColor()),
24 m_pointLabelsClipping(true),
25 m_lastHoveredMatchedPos{qQNaN(), qQNaN()},
26 m_mousePressed(false)
27{
28 setAcceptHoverEvents(true);
29 setFlag(flag: QGraphicsItem::ItemIsSelectable);
30 setZValue(ChartPresenter::SplineChartZValue);
31 connect(sender: m_series->d_func(), signal: &QXYSeriesPrivate::seriesUpdated,
32 context: this, slot: &SplineChartItem::handleSeriesUpdated);
33 connect(sender: series, signal: &QXYSeries::lightMarkerChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated);
34 connect(sender: series, signal: &QXYSeries::selectedLightMarkerChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated);
35 connect(sender: series, signal: &QXYSeries::markerSizeChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated);
36 connect(sender: series, signal: &QXYSeries::visibleChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated);
37 connect(sender: series, signal: &QXYSeries::opacityChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated);
38 connect(sender: series, signal: &QXYSeries::pointLabelsFormatChanged,
39 context: this, slot: &SplineChartItem::handleSeriesUpdated);
40 connect(sender: series, signal: &QXYSeries::pointLabelsVisibilityChanged,
41 context: this, slot: &SplineChartItem::handleSeriesUpdated);
42 connect(sender: series, signal: &QXYSeries::pointLabelsFontChanged,
43 context: this, slot: &SplineChartItem::handleSeriesUpdated);
44 connect(sender: series, signal: &QXYSeries::pointLabelsColorChanged,
45 context: this, slot: &SplineChartItem::handleSeriesUpdated);
46 connect(sender: series, signal: &QXYSeries::pointLabelsClippingChanged,
47 context: this, slot: &SplineChartItem::handleSeriesUpdated);
48 connect(sender: series, signal: &QSplineSeries::selectedColorChanged,
49 context: this, slot: &SplineChartItem::handleSeriesUpdated);
50 connect(sender: series, signal: &QLineSeries::selectedPointsChanged,
51 context: this, slot: &SplineChartItem::handleSeriesUpdated);
52 connect(sender: series, signal: &QSplineSeries::pointsConfigurationChanged,
53 context: this, slot: &SplineChartItem::handleSeriesUpdated);
54
55 handleSeriesUpdated();
56}
57
58QRectF SplineChartItem::boundingRect() const
59{
60 return m_rect;
61}
62
63QPainterPath SplineChartItem::shape() const
64{
65 return m_fullPath;
66}
67
68void SplineChartItem::setAnimation(SplineAnimation *animation)
69{
70 m_animation = animation;
71 XYChart::setAnimation(animation);
72}
73
74ChartAnimation *SplineChartItem::animation() const
75{
76 return m_animation;
77}
78
79void SplineChartItem::setControlGeometryPoints(const QList<QPointF> &points)
80{
81 m_controlPoints = points;
82}
83
84QList<QPointF> SplineChartItem::controlGeometryPoints() const
85{
86 return m_controlPoints;
87}
88
89void SplineChartItem::updateChart(const QList<QPointF> &oldPoints, const QList<QPointF> &newPoints,
90 int index)
91{
92 QList<QPointF> controlPoints;
93 if (newPoints.size() >= 2)
94 controlPoints = calculateControlPoints(points: newPoints);
95
96 if (m_animation)
97 m_animation->setup(oldPoints, newPoints, oldContorlPoints: m_controlPoints, newControlPoints: controlPoints, index);
98
99 m_points = newPoints;
100 m_controlPoints = controlPoints;
101 setDirty(false);
102
103 if (m_animation)
104 presenter()->startAnimation(animation: m_animation);
105 else
106 updateGeometry();
107}
108
109void SplineChartItem::updateGeometry()
110{
111 const QList<QPointF> &points = m_points;
112 const QList<QPointF> &controlPoints = m_controlPoints;
113
114 if ((points.size() < 2) || (controlPoints.size() < 2)) {
115 prepareGeometryChange();
116 m_path = QPainterPath();
117 m_rect = QRect();
118 return;
119 }
120
121 Q_ASSERT(points.size() * 2 - 2 == controlPoints.size());
122
123 QPainterPath splinePath;
124 QPainterPath fullPath;
125 // Use worst case scenario to determine required margin.
126 qreal margin = m_linePen.width() * 1.42;
127
128 if (m_series->chart()->chartType() == QChart::ChartTypePolar) {
129 QPainterPath splinePathLeft;
130 QPainterPath splinePathRight;
131 QPainterPath *currentSegmentPath = 0;
132 QPainterPath *previousSegmentPath = 0;
133 qreal minX = domain()->minX();
134 qreal maxX = domain()->maxX();
135 qreal minY = domain()->minY();
136 QPointF currentSeriesPoint = m_series->at(index: 0);
137 QPointF currentGeometryPoint = points.at(i: 0);
138 QPointF previousGeometryPoint = points.at(i: 0);
139 bool pointOffGrid = false;
140 bool previousPointWasOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX);
141 m_visiblePoints.clear();
142 m_visiblePoints.reserve(asize: points.size());
143
144 qreal domainRadius = domain()->size().height() / 2.0;
145 const QPointF centerPoint(domainRadius, domainRadius);
146
147 if (!previousPointWasOffGrid) {
148 fullPath.moveTo(p: points.at(i: 0));
149 // Do not draw points for points below minimum Y.
150 if (m_pointsVisible && currentSeriesPoint.y() >= minY)
151 m_visiblePoints.append(t: currentGeometryPoint);
152 }
153
154 qreal leftMarginLine = centerPoint.x() - margin;
155 qreal rightMarginLine = centerPoint.x() + margin;
156 qreal horizontal = centerPoint.y();
157
158 // See ScatterChartItem::updateGeometry() for explanation why seriesLastIndex is needed
159 const int seriesLastIndex = m_series->count() - 1;
160
161 for (int i = 1; i < points.size(); i++) {
162 // Interpolating spline fragments accurately is not trivial, and would anyway be ugly
163 // when thick pen is used, so we work around it by utilizing three separate
164 // paths for spline segments and clip those with custom regions at paint time.
165 // "Right" path contains segments that cross the axis line with visible point on the
166 // right side of the axis line, as well as segments that have one point within the margin
167 // on the right side of the axis line and another point on the right side of the chart.
168 // "Left" path contains points with similarly on the left side.
169 // "Full" path contains rest of the points.
170 // This doesn't yield perfect results always. E.g. when segment covers more than 90
171 // degrees and both of the points are within the margin, one in the top half and one in the
172 // bottom half of the chart, the bottom one gets clipped incorrectly.
173 // However, this should be rare occurrence in any sensible chart.
174 currentSeriesPoint = m_series->at(index: qMin(a: seriesLastIndex, b: i));
175 currentGeometryPoint = points.at(i);
176 pointOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX);
177
178 // Draw something unless both off-grid
179 if (!pointOffGrid || !previousPointWasOffGrid) {
180 bool dummyOk; // We know points are ok, but this is needed
181 qreal currentAngle = static_cast<PolarDomain *>(domain())->toAngularCoordinate(value: currentSeriesPoint.x(), ok&: dummyOk);
182 qreal previousAngle = static_cast<PolarDomain *>(domain())->toAngularCoordinate(value: m_series->at(index: i - 1).x(), ok&: dummyOk);
183
184 if ((qAbs(t: currentAngle - previousAngle) > 180.0)) {
185 // If the angle between two points is over 180 degrees (half X range),
186 // any direct segment between them becomes meaningless.
187 // In this case two line segments are drawn instead, from previous
188 // point to the center and from center to current point.
189 if ((previousAngle < 0.0 || (previousAngle <= 180.0 && previousGeometryPoint.x() < rightMarginLine))
190 && previousGeometryPoint.y() < horizontal) {
191 currentSegmentPath = &splinePathRight;
192 } else if ((previousAngle > 360.0 || (previousAngle > 180.0 && previousGeometryPoint.x() > leftMarginLine))
193 && previousGeometryPoint.y() < horizontal) {
194 currentSegmentPath = &splinePathLeft;
195 } else if (previousAngle > 0.0 && previousAngle < 360.0) {
196 currentSegmentPath = &splinePath;
197 } else {
198 currentSegmentPath = 0;
199 }
200
201 if (currentSegmentPath) {
202 if (previousSegmentPath != currentSegmentPath)
203 currentSegmentPath->moveTo(p: previousGeometryPoint);
204 if (!previousSegmentPath)
205 fullPath.moveTo(p: previousGeometryPoint);
206
207 currentSegmentPath->lineTo(p: centerPoint);
208 fullPath.lineTo(p: centerPoint);
209 }
210
211 previousSegmentPath = currentSegmentPath;
212
213 if ((currentAngle < 0.0 || (currentAngle <= 180.0 && currentGeometryPoint.x() < rightMarginLine))
214 && currentGeometryPoint.y() < horizontal) {
215 currentSegmentPath = &splinePathRight;
216 } else if ((currentAngle > 360.0 || (currentAngle > 180.0 &&currentGeometryPoint.x() > leftMarginLine))
217 && currentGeometryPoint.y() < horizontal) {
218 currentSegmentPath = &splinePathLeft;
219 } else if (currentAngle > 0.0 && currentAngle < 360.0) {
220 currentSegmentPath = &splinePath;
221 } else {
222 currentSegmentPath = 0;
223 }
224
225 if (currentSegmentPath) {
226 if (previousSegmentPath != currentSegmentPath)
227 currentSegmentPath->moveTo(p: centerPoint);
228 if (!previousSegmentPath)
229 fullPath.moveTo(p: centerPoint);
230
231 currentSegmentPath->lineTo(p: currentGeometryPoint);
232 fullPath.lineTo(p: currentGeometryPoint);
233 }
234 } else {
235 QPointF cp1 = controlPoints[2 * (i - 1)];
236 QPointF cp2 = controlPoints[(2 * i) - 1];
237
238 if (previousAngle < 0.0 || currentAngle < 0.0
239 || ((previousAngle <= 180.0 && currentAngle <= 180.0)
240 && ((previousGeometryPoint.x() < rightMarginLine && previousGeometryPoint.y() < horizontal)
241 || (currentGeometryPoint.x() < rightMarginLine && currentGeometryPoint.y() < horizontal)))) {
242 currentSegmentPath = &splinePathRight;
243 } else if (previousAngle > 360.0 || currentAngle > 360.0
244 || ((previousAngle > 180.0 && currentAngle > 180.0)
245 && ((previousGeometryPoint.x() > leftMarginLine && previousGeometryPoint.y() < horizontal)
246 || (currentGeometryPoint.x() > leftMarginLine && currentGeometryPoint.y() < horizontal)))) {
247 currentSegmentPath = &splinePathLeft;
248 } else {
249 currentSegmentPath = &splinePath;
250 }
251
252 if (currentSegmentPath != previousSegmentPath)
253 currentSegmentPath->moveTo(p: previousGeometryPoint);
254 if (!previousSegmentPath)
255 fullPath.moveTo(p: previousGeometryPoint);
256
257 fullPath.cubicTo(ctrlPt1: cp1, ctrlPt2: cp2, endPt: currentGeometryPoint);
258 currentSegmentPath->cubicTo(ctrlPt1: cp1, ctrlPt2: cp2, endPt: currentGeometryPoint);
259 }
260 } else {
261 currentSegmentPath = 0;
262 }
263
264 previousPointWasOffGrid = pointOffGrid;
265 if (!pointOffGrid && m_pointsVisible && currentSeriesPoint.y() >= minY)
266 m_visiblePoints.append(t: currentGeometryPoint);
267 previousSegmentPath = currentSegmentPath;
268 previousGeometryPoint = currentGeometryPoint;
269 }
270
271 m_pathPolarRight = splinePathRight;
272 m_pathPolarLeft = splinePathLeft;
273 // Note: This construction of m_fullpath is not perfect. The partial segments that are
274 // outside left/right clip regions at axis boundary still generate hover/click events,
275 // because shape doesn't get clipped. It doesn't seem possible to do sensibly.
276 } else { // not polar
277 splinePath.moveTo(p: points.at(i: 0));
278 for (int i = 0; i < points.size() - 1; i++) {
279 const QPointF &point = points.at(i: i + 1);
280 splinePath.cubicTo(ctrlPt1: controlPoints[2 * i], ctrlPt2: controlPoints[2 * i + 1], endPt: point);
281 }
282 fullPath = splinePath;
283 }
284
285 QPainterPathStroker stroker;
286 // The full path is comprised of three separate paths.
287 // This is why we are prepared for the "worst case" scenario, i.e. use always MiterJoin and
288 // multiply line width with square root of two when defining shape and bounding rectangle.
289 stroker.setWidth(margin);
290 stroker.setJoinStyle(Qt::MiterJoin);
291 stroker.setCapStyle(Qt::SquareCap);
292 stroker.setMiterLimit(m_linePen.miterLimit());
293
294 // Only zoom in if the bounding rects of the path fit inside int limits. QWidget::update() uses
295 // a region that has to be compatible with QRect.
296 QPainterPath checkShapePath = stroker.createStroke(path: fullPath);
297
298 // For mouse interactivity, we have to add the rects *after* the 'createStroke',
299 // as we don't need the outline - we need it filled up.
300 if (!m_series->lightMarker().isNull() || (!m_series->selectedLightMarker().isNull()
301 && !m_series->selectedPoints().isEmpty())) {
302 // +1, +2: a margin to guarantee we cover all of the pixmap
303 qreal markerHalfSize = (m_series->markerSize() / 2.0) + 1;
304 qreal markerSize = m_series->markerSize() + 2;
305
306 for (const auto &point : std::as_const(t: points)) {
307 checkShapePath.addRect(x: point.x() - markerHalfSize,
308 y: point.y() - markerHalfSize,
309 w: markerSize, h: markerSize);
310 }
311 }
312
313 if (checkShapePath.boundingRect().height() <= INT_MAX
314 && checkShapePath.boundingRect().width() <= INT_MAX
315 && splinePath.boundingRect().height() <= INT_MAX
316 && splinePath.boundingRect().width() <= INT_MAX) {
317 m_path = splinePath;
318
319 prepareGeometryChange();
320
321 m_fullPath = checkShapePath;
322 m_rect = m_fullPath.boundingRect();
323 }
324}
325
326/*!
327 Calculates control points which are needed by QPainterPath.cubicTo function to draw the cubic Bezier cureve between two points.
328 */
329QList<QPointF> SplineChartItem::calculateControlPoints(const QList<QPointF> &points)
330{
331 QList<QPointF> controlPoints;
332 controlPoints.resize(size: points.size() * 2 - 2);
333
334 int n = points.size() - 1;
335
336 if (n == 1) {
337 //for n==1
338 controlPoints[0].setX((2 * points[0].x() + points[1].x()) / 3);
339 controlPoints[0].setY((2 * points[0].y() + points[1].y()) / 3);
340 controlPoints[1].setX(2 * controlPoints[0].x() - points[0].x());
341 controlPoints[1].setY(2 * controlPoints[0].y() - points[0].y());
342 return controlPoints;
343 }
344
345 // Calculate first Bezier control points
346 // Set of equations for P0 to Pn points.
347 //
348 // | 2 1 0 0 ... 0 0 0 ... 0 0 0 | | P1_1 | | P0 + 2 * P1 |
349 // | 1 4 1 0 ... 0 0 0 ... 0 0 0 | | P1_2 | | 4 * P1 + 2 * P2 |
350 // | 0 1 4 1 ... 0 0 0 ... 0 0 0 | | P1_3 | | 4 * P2 + 2 * P3 |
351 // | . . . . . . . . . . . . | | ... | | ... |
352 // | 0 0 0 0 ... 1 4 1 ... 0 0 0 | * | P1_i | = | 4 * P(i-1) + 2 * Pi |
353 // | . . . . . . . . . . . . | | ... | | ... |
354 // | 0 0 0 0 0 0 0 0 ... 1 4 1 | | P1_(n-1)| | 4 * P(n-2) + 2 * P(n-1) |
355 // | 0 0 0 0 0 0 0 0 ... 0 2 7 | | P1_n | | 8 * P(n-1) + Pn |
356 //
357 QList<qreal> list;
358 list.resize(size: n);
359
360 list[0] = points[0].x() + 2 * points[1].x();
361
362 for (int i = 1; i < n - 1; ++i)
363 list[i] = 4 * points[i].x() + 2 * points[i + 1].x();
364
365 list[n - 1] = (8 * points[n - 1].x() + points[n].x()) / 2.0;
366
367 const QList<qreal> xControl = firstControlPoints(list);
368
369 list[0] = points[0].y() + 2 * points[1].y();
370
371 for (int i = 1; i < n - 1; ++i)
372 list[i] = 4 * points[i].y() + 2 * points[i + 1].y();
373
374 list[n - 1] = (8 * points[n - 1].y() + points[n].y()) / 2.0;
375
376 const QList<qreal> yControl = firstControlPoints(list);
377
378 for (int i = 0, j = 0; i < n; ++i, ++j) {
379
380 controlPoints[j].setX(xControl[i]);
381 controlPoints[j].setY(yControl[i]);
382
383 j++;
384
385 if (i < n - 1) {
386 controlPoints[j].setX(2 * points[i + 1].x() - xControl[i + 1]);
387 controlPoints[j].setY(2 * points[i + 1].y() - yControl[i + 1]);
388 } else {
389 controlPoints[j].setX((points[n].x() + xControl[n - 1]) / 2);
390 controlPoints[j].setY((points[n].y() + yControl[n - 1]) / 2);
391 }
392 }
393 return controlPoints;
394}
395
396QList<qreal> SplineChartItem::firstControlPoints(const QList<qreal> &list)
397{
398 QList<qreal> result;
399
400 int count = list.size();
401 result.resize(size: count);
402 result[0] = list[0] / 2.0;
403
404 QList<qreal> temp;
405 temp.resize(size: count);
406 temp[0] = 0;
407
408 qreal b = 2.0;
409
410 for (int i = 1; i < count; i++) {
411 temp[i] = 1 / b;
412 b = (i < count - 1 ? 4.0 : 3.5) - temp[i];
413 result[i] = (list[i] - result[i - 1]) / b;
414 }
415
416 for (int i = 1; i < count; i++)
417 result[count - i - 1] -= temp[count - i] * result[count - i];
418
419 return result;
420}
421
422//handlers
423
424void SplineChartItem::handleSeriesUpdated()
425{
426 setVisible(m_series->isVisible());
427 setOpacity(m_series->opacity());
428 m_pointsVisible = m_series->pointsVisible();
429 m_linePen = m_series->pen();
430 m_pointPen = m_series->pen();
431 m_pointPen.setWidthF(2 * m_pointPen.width());
432 m_pointLabelsFormat = m_series->pointLabelsFormat();
433 m_pointLabelsVisible = m_series->pointLabelsVisible();
434 m_markerSize = m_series->markerSize();
435 m_pointLabelsFont = m_series->pointLabelsFont();
436 m_pointLabelsColor = m_series->pointLabelsColor();
437 m_selectedPoints = m_series->selectedPoints();
438 m_selectedColor = m_series->selectedColor();
439 bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping();
440 m_pointLabelsClipping = m_series->pointLabelsClipping();
441 // Update whole chart in case label clipping changed as labels can be outside series area
442 if (labelClippingChanged)
443 m_series->chart()->update();
444 else
445 update();
446}
447
448//painter
449
450void SplineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
451{
452 Q_UNUSED(widget);
453 Q_UNUSED(option);
454
455 QRectF clipRect = QRectF(QPointF(0, 0), domain()->size());
456
457 painter->save();
458 painter->setPen(m_linePen);
459 painter->setBrush(Qt::NoBrush);
460
461 if (m_series->chart()->chartType() == QChart::ChartTypePolar) {
462 qreal halfWidth = domain()->size().width() / 2.;
463 QRectF clipRectLeft = QRectF(0, 0, halfWidth, domain()->size().height());
464 QRectF clipRectRight = QRectF(halfWidth, 0, halfWidth, domain()->size().height());
465 QRegion fullPolarClipRegion(clipRect.toRect(), QRegion::Ellipse);
466 QRegion clipRegionLeft(fullPolarClipRegion.intersected(r: clipRectLeft.toRect()));
467 QRegion clipRegionRight(fullPolarClipRegion.intersected(r: clipRectRight.toRect()));
468 painter->setClipRegion(clipRegionLeft);
469 painter->drawPath(path: m_pathPolarLeft);
470 painter->setClipRegion(clipRegionRight);
471 painter->drawPath(path: m_pathPolarRight);
472 painter->setClipRegion(fullPolarClipRegion);
473 } else {
474 painter->setClipRect(clipRect);
475 }
476
477 if (m_series->bestFitLineVisible())
478 m_series->d_func()->drawBestFitLine(painter, clipRect);
479
480 painter->drawPath(path: m_path);
481
482 int pointLabelsOffset = m_linePen.width() / 2;
483
484 // Draw markers if a marker or marker for selected points only has been
485 // set (set to QImage() to disable)
486 if (!m_series->lightMarker().isNull() || (!m_series->selectedLightMarker().isNull()
487 && !m_series->selectedPoints().isEmpty())) {
488 const QImage &marker = m_series->lightMarker();
489 const QImage &selectedMarker = m_series->selectedLightMarker();
490 qreal markerHalfSize = m_markerSize / 2.0;
491 pointLabelsOffset = markerHalfSize;
492
493 for (int i = 0; i < m_points.size(); ++i) {
494 // Documentation of light markers says that points visibility and
495 // light markers are independent features. Therefore m_pointsVisible
496 // is not used here as light markers are drawn if lightMarker is not null.
497 // However points visibility configuration can be still used here.
498 bool drawPoint = !m_series->lightMarker().isNull();
499 if (m_pointsConfiguration.contains(key: i)) {
500 const auto &conf = m_pointsConfiguration[i];
501
502 if (conf.contains(key: QXYSeries::PointConfiguration::Visibility)) {
503 drawPoint = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility]
504 .toBool();
505 }
506 }
507
508 bool drawSelectedPoint = false;
509 if (m_series->isPointSelected(index: i)) {
510 drawPoint = true;
511 drawSelectedPoint = !selectedMarker.isNull();
512 }
513 if (drawPoint) {
514 const QRectF rect(m_points[i].x() - markerHalfSize,
515 m_points[i].y() - markerHalfSize,
516 m_markerSize, m_markerSize);
517 painter->drawImage(r: rect, image: drawSelectedPoint ? selectedMarker : marker);
518 }
519 }
520 }
521
522 if (m_pointLabelsVisible) {
523 if (m_pointLabelsClipping)
524 painter->setClipping(true);
525 else
526 painter->setClipping(false);
527 m_series->d_func()->drawSeriesPointLabels(painter, points: m_points, offset: pointLabelsOffset);
528 }
529
530 painter->setPen(m_pointPen);
531 if (m_series->chart()->chartType() == QChart::ChartTypePolar && m_pointsVisible) {
532 painter->drawPoints(points: m_visiblePoints);
533 } else {
534 const bool simpleDraw = m_selectedPoints.isEmpty() && m_pointsConfiguration.isEmpty();
535 painter->setPen(Qt::NoPen);
536 painter->setBrush(m_linePen.color());
537 painter->setClipping(true);
538
539 if (m_pointsVisible && simpleDraw && m_series->lightMarker().isNull()) {
540 for (int i = 0; i < m_points.size(); ++i)
541 painter->drawEllipse(center: m_points.at(i), rx: m_markerSize, ry: m_markerSize);
542 } else if (!simpleDraw) {
543 qreal ptSize = m_markerSize;
544 for (int i = 0; i < m_points.size(); ++i) {
545 if (clipRect.contains(p: m_points.at(i))) {
546 painter->save();
547 ptSize = m_markerSize;
548 bool drawPoint = m_pointsVisible && m_series->lightMarker().isNull();
549 if (m_pointsConfiguration.contains(key: i)) {
550 const auto &conf = m_pointsConfiguration[i];
551 if (conf.contains(key: QXYSeries::PointConfiguration::Visibility)) {
552 drawPoint =
553 m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility]
554 .toBool();
555 }
556
557 if (drawPoint) {
558 if (conf.contains(key: QXYSeries::PointConfiguration::Size)) {
559 ptSize = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Size]
560 .toReal();
561 }
562
563 if (conf.contains(key: QXYSeries::PointConfiguration::Color)) {
564 painter->setBrush(
565 m_pointsConfiguration[i][QXYSeries::PointConfiguration::Color]
566 .value<QColor>());
567 }
568 }
569 }
570
571 if (m_series->isPointSelected(index: i)) {
572 // Selected points are drawn regardless of m_pointsVisible settings and
573 // custom point configuration. However, they are not drawn if light markers
574 // are used. The reason of this is to avoid displaying selected point
575 // over selected light marker.
576 drawPoint = m_series->selectedLightMarker().isNull();
577 ptSize = ptSize * 1.5;
578 if (m_selectedColor.isValid())
579 painter->setBrush(m_selectedColor);
580 }
581
582 if (drawPoint)
583 painter->drawEllipse(center: m_points.at(i), rx: ptSize, ry: ptSize);
584 painter->restore();
585 }
586 }
587 }
588 }
589 painter->restore();
590}
591
592void SplineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
593{
594 QPointF matchedP = matchForLightMarker(eventPos: event->pos());
595 if (!qIsNaN(d: matchedP.x()))
596 emit XYChart::pressed(point: matchedP);
597 else
598 emit XYChart::pressed(point: domain()->calculateDomainPoint(point: event->pos()));
599
600 m_lastMousePos = event->pos();
601 m_mousePressed = true;
602 QGraphicsItem::mousePressEvent(event);
603}
604
605void SplineChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
606{
607 // Identical code in LineChartItem
608 const QPointF matchedP = hoverPoint(eventPos: event->pos());
609 m_lastHoveredMatchedPos = matchedP;
610 emit XYChart::hovered(point: matchedP, state: true);
611
612 QGraphicsItem::hoverEnterEvent(event);
613}
614
615void SplineChartItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
616{
617 const QPointF matchedP = hoverPoint(eventPos: event->pos());
618 if (!fuzzyComparePointF(p1: matchedP, p2: m_lastHoveredMatchedPos)) {
619 emit XYChart::hovered(point: matchedP, state: true);
620 m_lastHoveredMatchedPos = matchedP;
621 }
622
623 QGraphicsItem::hoverMoveEvent(event);
624}
625
626void SplineChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
627{
628 const QPointF matchedP = hoverPoint(eventPos: event->pos());
629 emit XYChart::hovered(point: matchedP, state: false);
630 m_lastHoveredMatchedPos = {qQNaN(), qQNaN()};
631
632 QGraphicsItem::hoverLeaveEvent(event);
633}
634
635void SplineChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
636{
637 QPointF result;
638 QPointF matchedP = matchForLightMarker(eventPos: m_lastMousePos);
639 if (!qIsNaN(d: matchedP.x()))
640 result = matchedP;
641 else
642 result = domain()->calculateDomainPoint(point: m_lastMousePos);
643
644 emit XYChart::released(point: result);
645 if (m_mousePressed)
646 emit XYChart::clicked(point: result);
647 m_mousePressed = false;
648 QGraphicsItem::mouseReleaseEvent(event);
649}
650
651void SplineChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
652{
653 QPointF matchedP = matchForLightMarker(eventPos: event->pos());
654 if (!qIsNaN(d: matchedP.x()))
655 emit XYChart::doubleClicked(point: matchedP);
656 else
657 emit XYChart::doubleClicked(point: domain()->calculateDomainPoint(point: m_lastMousePos));
658
659 QGraphicsItem::mouseDoubleClickEvent(event);
660}
661
662QT_END_NAMESPACE
663
664#include "moc_splinechartitem_p.cpp"
665

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