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