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 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | SplineChartItem::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 | |
58 | QRectF SplineChartItem::boundingRect() const |
59 | { |
60 | return m_rect; |
61 | } |
62 | |
63 | QPainterPath SplineChartItem::shape() const |
64 | { |
65 | return m_fullPath; |
66 | } |
67 | |
68 | void SplineChartItem::setAnimation(SplineAnimation *animation) |
69 | { |
70 | m_animation = animation; |
71 | XYChart::setAnimation(animation); |
72 | } |
73 | |
74 | ChartAnimation *SplineChartItem::animation() const |
75 | { |
76 | return m_animation; |
77 | } |
78 | |
79 | void SplineChartItem::setControlGeometryPoints(const QList<QPointF> &points) |
80 | { |
81 | m_controlPoints = points; |
82 | } |
83 | |
84 | QList<QPointF> SplineChartItem::controlGeometryPoints() const |
85 | { |
86 | return m_controlPoints; |
87 | } |
88 | |
89 | void 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 | |
109 | void 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 &¤tGeometryPoint.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 | */ |
329 | QList<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 | |
396 | QList<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 | |
424 | void 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 | |
450 | void 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 | |
592 | void 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 | |
605 | void 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 | |
615 | void 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 | |
626 | void 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 | |
635 | void 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 | |
651 | void 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 | |
662 | QT_END_NAMESPACE |
663 | |
664 | #include "moc_splinechartitem_p.cpp" |
665 | |