1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include <private/linechartitem_p.h> |
5 | #include <QtCharts/QLineSeries> |
6 | #include <private/qlineseries_p.h> |
7 | #include <private/chartpresenter_p.h> |
8 | #include <private/polardomain_p.h> |
9 | #include <private/chartthememanager_p.h> |
10 | #include <private/charttheme_p.h> |
11 | #include <QtGui/QPainter> |
12 | #include <QtWidgets/QGraphicsSceneMouseEvent> |
13 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | LineChartItem::LineChartItem(QLineSeries *series, QGraphicsItem *item) |
17 | : XYChart(series,item), |
18 | m_series(series), |
19 | m_pointsVisible(false), |
20 | m_chartType(QChart::ChartTypeUndefined), |
21 | m_pointLabelsVisible(false), |
22 | m_markerSize(series->markerSize()), |
23 | m_pointLabelsFormat(series->pointLabelsFormat()), |
24 | m_pointLabelsFont(series->pointLabelsFont()), |
25 | m_pointLabelsColor(series->pointLabelsColor()), |
26 | m_pointLabelsClipping(true), |
27 | m_lastHoveredMatchedPos{qQNaN(), qQNaN()}, |
28 | m_mousePressed(false) |
29 | { |
30 | setAcceptHoverEvents(true); |
31 | setFlag(flag: QGraphicsItem::ItemIsSelectable); |
32 | setZValue(ChartPresenter::LineChartZValue); |
33 | connect(sender: series->d_func(), signal: &QXYSeriesPrivate::seriesUpdated, |
34 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
35 | connect(sender: series, signal: &QXYSeries::lightMarkerChanged, context: this, slot: &LineChartItem::handleSeriesUpdated); |
36 | connect(sender: series, signal: &QXYSeries::selectedLightMarkerChanged, context: this, slot: &LineChartItem::handleSeriesUpdated); |
37 | connect(sender: series, signal: &QXYSeries::markerSizeChanged, context: this, slot: &LineChartItem::handleSeriesUpdated); |
38 | connect(sender: series, signal: &QXYSeries::visibleChanged, context: this, slot: &LineChartItem::handleSeriesUpdated); |
39 | connect(sender: series, signal: &QXYSeries::opacityChanged, context: this, slot: &LineChartItem::handleSeriesUpdated); |
40 | connect(sender: series, signal: &QXYSeries::pointLabelsFormatChanged, |
41 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
42 | connect(sender: series, signal: &QXYSeries::pointLabelsVisibilityChanged, |
43 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
44 | connect(sender: series, signal: &QXYSeries::pointLabelsFontChanged, |
45 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
46 | connect(sender: series, signal: &QXYSeries::pointLabelsColorChanged, |
47 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
48 | connect(sender: series, signal: &QXYSeries::pointLabelsClippingChanged, |
49 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
50 | connect(sender: series, signal: &QLineSeries::selectedColorChanged, |
51 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
52 | connect(sender: series, signal: &QLineSeries::selectedPointsChanged, |
53 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
54 | connect(sender: series, signal: &QLineSeries::pointsConfigurationChanged, |
55 | context: this, slot: &LineChartItem::handleSeriesUpdated); |
56 | |
57 | handleSeriesUpdated(); |
58 | } |
59 | |
60 | QRectF LineChartItem::boundingRect() const |
61 | { |
62 | return m_rect; |
63 | } |
64 | |
65 | QPainterPath LineChartItem::shape() const |
66 | { |
67 | return m_shapePath; |
68 | } |
69 | |
70 | void LineChartItem::updateGeometry() |
71 | { |
72 | if (m_series->useOpenGL()) { |
73 | if (!m_rect.isEmpty()) { |
74 | prepareGeometryChange(); |
75 | // Changed signal seems to trigger even with empty region |
76 | m_rect = QRectF(); |
77 | } |
78 | update(); |
79 | return; |
80 | } |
81 | |
82 | // Store the points to a local variable so that the old line gets properly cleared |
83 | // when animation starts. |
84 | m_linePoints = geometryPoints(); |
85 | const QList<QPointF> &points = m_linePoints; |
86 | |
87 | if (points.size() == 0) { |
88 | prepareGeometryChange(); |
89 | m_fullPath = QPainterPath(); |
90 | m_linePath = QPainterPath(); |
91 | m_rect = QRect(); |
92 | return; |
93 | } |
94 | |
95 | QPainterPath linePath; |
96 | QPainterPath fullPath; |
97 | // Use worst case scenario to determine required margin. |
98 | qreal margin = m_linePen.width() * 1.42; |
99 | |
100 | // Area series use component line series that aren't necessarily added to the chart themselves, |
101 | // so check if chart type is forced before trying to obtain it from the chart. |
102 | QChart::ChartType chartType = m_chartType; |
103 | if (chartType == QChart::ChartTypeUndefined) |
104 | chartType = m_series->chart()->chartType(); |
105 | |
106 | // For polar charts, we need special handling for angular (horizontal) |
107 | // points that are off-grid. |
108 | if (chartType == QChart::ChartTypePolar) { |
109 | QPainterPath linePathLeft; |
110 | QPainterPath linePathRight; |
111 | QPainterPath *currentSegmentPath = 0; |
112 | QPainterPath *previousSegmentPath = 0; |
113 | qreal minX = domain()->minX(); |
114 | qreal maxX = domain()->maxX(); |
115 | qreal minY = domain()->minY(); |
116 | QPointF currentSeriesPoint = m_series->at(index: 0); |
117 | QPointF currentGeometryPoint = points.at(i: 0); |
118 | QPointF previousGeometryPoint = points.at(i: 0); |
119 | bool pointOffGrid = false; |
120 | bool previousPointWasOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); |
121 | |
122 | qreal domainRadius = domain()->size().height() / 2.0; |
123 | const QPointF centerPoint(domainRadius, domainRadius); |
124 | |
125 | if (!previousPointWasOffGrid) { |
126 | fullPath.moveTo(p: points.at(i: 0)); |
127 | if (m_pointsVisible && currentSeriesPoint.y() >= minY) { |
128 | // Do not draw ellipses for points below minimum Y. |
129 | linePath.addEllipse(center: points.at(i: 0), rx: m_markerSize, ry: m_markerSize); |
130 | fullPath.addEllipse(center: points.at(i: 0), rx: m_markerSize, ry: m_markerSize); |
131 | linePath.moveTo(p: points.at(i: 0)); |
132 | fullPath.moveTo(p: points.at(i: 0)); |
133 | } |
134 | } |
135 | |
136 | qreal leftMarginLine = centerPoint.x() - margin; |
137 | qreal rightMarginLine = centerPoint.x() + margin; |
138 | qreal horizontal = centerPoint.y(); |
139 | |
140 | // See ScatterChartItem::updateGeometry() for explanation why seriesLastIndex is needed |
141 | const int seriesLastIndex = m_series->count() - 1; |
142 | |
143 | for (int i = 1; i < points.size(); i++) { |
144 | // Interpolating line fragments would be ugly when thick pen is used, |
145 | // so we work around it by utilizing three separate |
146 | // paths for line segments and clip those with custom regions at paint time. |
147 | // "Right" path contains segments that cross the axis line with visible point on the |
148 | // right side of the axis line, as well as segments that have one point within the margin |
149 | // on the right side of the axis line and another point on the right side of the chart. |
150 | // "Left" path contains points with similarly on the left side. |
151 | // "Full" path contains rest of the points. |
152 | // This doesn't yield perfect results always. E.g. when segment covers more than 90 |
153 | // degrees and both of the points are within the margin, one in the top half and one in the |
154 | // bottom half of the chart, the bottom one gets clipped incorrectly. |
155 | // However, this should be rare occurrence in any sensible chart. |
156 | currentSeriesPoint = m_series->at(index: qMin(a: seriesLastIndex, b: i)); |
157 | currentGeometryPoint = points.at(i); |
158 | pointOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); |
159 | |
160 | // Draw something unless both off-grid |
161 | if (!pointOffGrid || !previousPointWasOffGrid) { |
162 | QPointF intersectionPoint; |
163 | qreal y; |
164 | if (pointOffGrid != previousPointWasOffGrid) { |
165 | if (currentGeometryPoint.x() == previousGeometryPoint.x()) { |
166 | y = currentGeometryPoint.y() + (currentGeometryPoint.y() - previousGeometryPoint.y()) / 2.0; |
167 | } else { |
168 | qreal ratio = (centerPoint.x() - currentGeometryPoint.x()) / (currentGeometryPoint.x() - previousGeometryPoint.x()); |
169 | y = currentGeometryPoint.y() + (currentGeometryPoint.y() - previousGeometryPoint.y()) * ratio; |
170 | } |
171 | intersectionPoint = QPointF(centerPoint.x(), y); |
172 | } |
173 | |
174 | bool dummyOk; // We know points are ok, but this is needed |
175 | qreal currentAngle = 0; |
176 | qreal previousAngle = 0; |
177 | if (const PolarDomain *pd = qobject_cast<const PolarDomain *>(object: domain())) { |
178 | currentAngle = pd->toAngularCoordinate(value: currentSeriesPoint.x(), ok&: dummyOk); |
179 | previousAngle = pd->toAngularCoordinate(value: m_series->at(index: i - 1).x(), ok&: dummyOk); |
180 | } else { |
181 | qWarning() << Q_FUNC_INFO << "Unexpected domain: " << domain(); |
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 = &linePathRight; |
191 | } else if ((previousAngle > 360.0 || (previousAngle > 180.0 && previousGeometryPoint.x() > leftMarginLine)) |
192 | && previousGeometryPoint.y() < horizontal) { |
193 | currentSegmentPath = &linePathLeft; |
194 | } else if (previousAngle > 0.0 && previousAngle < 360.0) { |
195 | currentSegmentPath = &linePath; |
196 | } else { |
197 | currentSegmentPath = 0; |
198 | } |
199 | |
200 | if (currentSegmentPath) { |
201 | if (previousSegmentPath != currentSegmentPath) |
202 | currentSegmentPath->moveTo(p: previousGeometryPoint); |
203 | if (previousPointWasOffGrid) |
204 | fullPath.moveTo(p: intersectionPoint); |
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 = &linePathRight; |
215 | } else if ((currentAngle > 360.0 || (currentAngle > 180.0 &¤tGeometryPoint.x() > leftMarginLine)) |
216 | && currentGeometryPoint.y() < horizontal) { |
217 | currentSegmentPath = &linePathLeft; |
218 | } else if (currentAngle > 0.0 && currentAngle < 360.0) { |
219 | currentSegmentPath = &linePath; |
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 | if (pointOffGrid) |
232 | fullPath.lineTo(p: intersectionPoint); |
233 | else |
234 | fullPath.lineTo(p: currentGeometryPoint); |
235 | } |
236 | } else { |
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 = &linePathRight; |
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 = &linePathLeft; |
247 | } else { |
248 | currentSegmentPath = &linePath; |
249 | } |
250 | |
251 | if (currentSegmentPath != previousSegmentPath) |
252 | currentSegmentPath->moveTo(p: previousGeometryPoint); |
253 | if (previousPointWasOffGrid) |
254 | fullPath.moveTo(p: intersectionPoint); |
255 | |
256 | if (pointOffGrid) |
257 | fullPath.lineTo(p: intersectionPoint); |
258 | else |
259 | fullPath.lineTo(p: currentGeometryPoint); |
260 | currentSegmentPath->lineTo(p: currentGeometryPoint); |
261 | } |
262 | } else { |
263 | currentSegmentPath = 0; |
264 | } |
265 | |
266 | previousPointWasOffGrid = pointOffGrid; |
267 | if (m_pointsVisible && !pointOffGrid && currentSeriesPoint.y() >= minY) { |
268 | linePath.addEllipse(center: points.at(i), rx: m_markerSize, ry: m_markerSize); |
269 | fullPath.addEllipse(center: points.at(i), rx: m_markerSize, ry: m_markerSize); |
270 | linePath.moveTo(p: points.at(i)); |
271 | fullPath.moveTo(p: points.at(i)); |
272 | } |
273 | previousSegmentPath = currentSegmentPath; |
274 | previousGeometryPoint = currentGeometryPoint; |
275 | } |
276 | m_linePathPolarRight = linePathRight; |
277 | m_linePathPolarLeft = linePathLeft; |
278 | // Note: This construction of m_fullpath is not perfect. The partial segments that are |
279 | // outside left/right clip regions at axis boundary still generate hover/click events, |
280 | // because shape doesn't get clipped. It doesn't seem possible to do sensibly. |
281 | } else { // not polar |
282 | linePath.moveTo(p: points.at(i: 0)); |
283 | for (int i = 1; i < points.size(); i++) |
284 | linePath.lineTo(p: points.at(i)); |
285 | fullPath = linePath; |
286 | } |
287 | |
288 | QPainterPathStroker stroker; |
289 | // QPainter::drawLine does not respect join styles, for example BevelJoin becomes MiterJoin. |
290 | // This is why we are prepared for the "worst case" scenario, i.e. use always MiterJoin and |
291 | // multiply line width with square root of two when defining shape and bounding rectangle. |
292 | stroker.setWidth(margin); |
293 | stroker.setJoinStyle(Qt::MiterJoin); |
294 | stroker.setCapStyle(Qt::SquareCap); |
295 | stroker.setMiterLimit(m_linePen.miterLimit()); |
296 | |
297 | QPainterPath checkShapePath = stroker.createStroke(path: fullPath); |
298 | |
299 | // For mouse interactivity, we have to add the rects *after* the 'createStroke', |
300 | // as we don't need the outline - we need it filled up. |
301 | if (!m_series->lightMarker().isNull() |
302 | || (!m_series->selectedLightMarker().isNull() |
303 | && !m_series->selectedPoints().isEmpty())) { |
304 | // +1, +2: a margin to guarantee we cover all of the pixmap |
305 | qreal markerHalfSize = (m_markerSize / 2.0) + 1; |
306 | qreal markerSize = m_markerSize + 2; |
307 | |
308 | for (const auto &point : std::as_const(t&: m_linePoints)) { |
309 | checkShapePath.addRect(x: point.x() - markerHalfSize, |
310 | y: point.y() - markerHalfSize, |
311 | w: markerSize, h: markerSize); |
312 | } |
313 | } |
314 | |
315 | // Only zoom in if the bounding rects of the paths fit inside int limits. QWidget::update() uses |
316 | // a region that has to be compatible with QRect. |
317 | if (checkShapePath.boundingRect().height() <= INT_MAX |
318 | && checkShapePath.boundingRect().width() <= INT_MAX |
319 | && linePath.boundingRect().height() <= INT_MAX |
320 | && linePath.boundingRect().width() <= INT_MAX |
321 | && fullPath.boundingRect().height() <= INT_MAX |
322 | && fullPath.boundingRect().width() <= INT_MAX) { |
323 | prepareGeometryChange(); |
324 | |
325 | m_linePath = linePath; |
326 | m_fullPath = fullPath; |
327 | m_shapePath = checkShapePath; |
328 | |
329 | m_rect = m_shapePath.boundingRect(); |
330 | } else { |
331 | update(); |
332 | } |
333 | } |
334 | |
335 | void LineChartItem::handleSeriesUpdated() |
336 | { |
337 | bool doGeometryUpdate = |
338 | (m_pointsVisible != m_series->pointsVisible()) |
339 | || (m_series->pointsVisible() |
340 | && (m_linePen != m_series->pen() |
341 | || m_selectedColor != m_series->selectedColor() |
342 | || m_selectedPoints != m_series->selectedPoints())) |
343 | || m_series->pointsConfiguration() != m_pointsConfiguration |
344 | || (m_markerSize != m_series->markerSize()); |
345 | bool visibleChanged = m_series->isVisible() != isVisible(); |
346 | setVisible(m_series->isVisible()); |
347 | setOpacity(m_series->opacity()); |
348 | m_pointsVisible = m_series->pointsVisible(); |
349 | |
350 | qreal seriesPenWidth = m_series->pen().widthF(); |
351 | if (m_series->d_func()->isMarkerSizeDefault() |
352 | && (!qFuzzyCompare(p1: seriesPenWidth, p2: m_linePen.widthF()))) { |
353 | m_series->d_func()->setMarkerSize(seriesPenWidth * 1.5); |
354 | } |
355 | m_linePen = m_series->pen(); |
356 | m_markerSize = m_series->markerSize(); |
357 | m_pointLabelsFormat = m_series->pointLabelsFormat(); |
358 | m_pointLabelsVisible = m_series->pointLabelsVisible(); |
359 | m_pointLabelsFont = m_series->pointLabelsFont(); |
360 | m_pointLabelsColor = m_series->pointLabelsColor(); |
361 | m_selectedColor = m_series->selectedColor(); |
362 | m_selectedPoints = m_series->selectedPoints(); |
363 | m_pointsConfiguration = m_series->pointsConfiguration(); |
364 | bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping(); |
365 | m_pointLabelsClipping = m_series->pointLabelsClipping(); |
366 | if (doGeometryUpdate) |
367 | updateGeometry(); |
368 | else if (m_series->useOpenGL() && visibleChanged) |
369 | refreshGlChart(); |
370 | |
371 | // Update whole chart in case label clipping changed as labels can be outside series area |
372 | if (labelClippingChanged) |
373 | m_series->chart()->update(); |
374 | else |
375 | update(); |
376 | } |
377 | |
378 | void LineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) |
379 | { |
380 | Q_UNUSED(widget); |
381 | Q_UNUSED(option); |
382 | |
383 | if (m_series->useOpenGL()) |
384 | return; |
385 | |
386 | QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); |
387 | // Adjust clip rect half a pixel in required dimensions to make it include lines along the |
388 | // plot area edges, but never increase clip so much that any portion of the line is draw beyond |
389 | // the plot area. |
390 | const qreal x1 = pos().x() - int(pos().x()); |
391 | const qreal y1 = pos().y() - int(pos().y()); |
392 | const qreal x2 = (clipRect.width() + 0.5) - int(clipRect.width() + 0.5); |
393 | const qreal y2 = (clipRect.height() + 0.5) - int(clipRect.height() + 0.5); |
394 | clipRect.adjust(xp1: -x1, yp1: -y1, xp2: qMax(a: x1, b: x2), yp2: qMax(a: y1, b: y2)); |
395 | |
396 | painter->save(); |
397 | painter->setPen(m_linePen); |
398 | bool alwaysUsePath = false; |
399 | |
400 | if (m_series->chart()->chartType() == QChart::ChartTypePolar) { |
401 | qreal halfWidth = domain()->size().width() / 2.0; |
402 | QRectF clipRectLeft = QRectF(0, 0, halfWidth, domain()->size().height()); |
403 | QRectF clipRectRight = QRectF(halfWidth, 0, halfWidth, domain()->size().height()); |
404 | QRegion fullPolarClipRegion(clipRect.toRect(), QRegion::Ellipse); |
405 | QRegion clipRegionLeft(fullPolarClipRegion.intersected(r: clipRectLeft.toRect())); |
406 | QRegion clipRegionRight(fullPolarClipRegion.intersected(r: clipRectRight.toRect())); |
407 | painter->setClipRegion(clipRegionLeft); |
408 | painter->drawPath(path: m_linePathPolarLeft); |
409 | painter->setClipRegion(clipRegionRight); |
410 | painter->drawPath(path: m_linePathPolarRight); |
411 | painter->setClipRegion(fullPolarClipRegion); |
412 | alwaysUsePath = true; // required for proper clipping |
413 | } else { |
414 | painter->setClipRect(clipRect); |
415 | } |
416 | |
417 | if (m_series->bestFitLineVisible()) |
418 | m_series->d_func()->drawBestFitLine(painter, clipRect); |
419 | |
420 | if (m_linePen.style() != Qt::SolidLine || alwaysUsePath) { |
421 | // If pen style is not solid line, use path painting to ensure proper pattern continuity |
422 | painter->drawPath(path: m_linePath); |
423 | } else { |
424 | for (int i = 1; i < m_linePoints.size(); ++i) |
425 | painter->drawLine(p1: m_linePoints.at(i: i - 1), p2: m_linePoints.at(i)); |
426 | } |
427 | |
428 | int pointLabelsOffset = m_linePen.width() / 2; |
429 | |
430 | // Draw markers if a marker or marker for selected points only has been |
431 | // set (set to QImage() to disable) |
432 | if (!m_series->lightMarker().isNull() || (!m_series->selectedLightMarker().isNull() |
433 | && !m_series->selectedPoints().isEmpty())) { |
434 | const QImage &marker = m_series->lightMarker(); |
435 | const QImage &selectedMarker = m_series->selectedLightMarker(); |
436 | qreal markerHalfSize = m_markerSize / 2.0; |
437 | pointLabelsOffset = markerHalfSize; |
438 | |
439 | for (int i = 0; i < m_linePoints.size(); ++i) { |
440 | // Documentation of light markers says that points visibility and |
441 | // light markers are independent features. Therefore m_pointsVisible |
442 | // is not used here as light markers are drawn if lightMarker is not null. |
443 | // However points visibility configuration can be still used here. |
444 | bool drawPoint = !m_series->lightMarker().isNull(); |
445 | if (m_pointsConfiguration.contains(key: i)) { |
446 | const auto &conf = m_pointsConfiguration[i]; |
447 | |
448 | if (conf.contains(key: QXYSeries::PointConfiguration::Visibility)) { |
449 | drawPoint = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility] |
450 | .toBool(); |
451 | } |
452 | } |
453 | |
454 | bool drawSelectedPoint = false; |
455 | if (m_series->isPointSelected(index: i)) { |
456 | drawPoint = true; |
457 | drawSelectedPoint = !selectedMarker.isNull(); |
458 | } |
459 | if (drawPoint) { |
460 | const QRectF rect(m_linePoints[i].x() - markerHalfSize, |
461 | m_linePoints[i].y() - markerHalfSize, |
462 | m_markerSize, m_markerSize); |
463 | painter->drawImage(r: rect, image: drawSelectedPoint ? selectedMarker : marker); |
464 | } |
465 | } |
466 | } |
467 | |
468 | m_series->d_func()->drawPointLabels(painter, allPoints: m_linePoints, offset: pointLabelsOffset); |
469 | |
470 | const bool simpleDraw = m_selectedPoints.isEmpty() && m_pointsConfiguration.isEmpty(); |
471 | |
472 | painter->setPen(Qt::NoPen); |
473 | painter->setBrush(m_linePen.color()); |
474 | painter->setClipping(true); |
475 | if (m_pointsVisible && simpleDraw && m_series->lightMarker().isNull()) { |
476 | for (int i = 0; i < m_linePoints.size(); ++i) |
477 | painter->drawEllipse(center: m_linePoints.at(i), rx: m_markerSize, ry: m_markerSize); |
478 | } else if (!simpleDraw) { |
479 | qreal ptSize = m_markerSize; |
480 | for (int i = 0; i < m_linePoints.size(); ++i) { |
481 | if (clipRect.contains(p: m_linePoints.at(i))) { |
482 | painter->save(); |
483 | ptSize = m_markerSize; |
484 | bool drawPoint = m_pointsVisible && m_series->lightMarker().isNull(); |
485 | if (m_pointsConfiguration.contains(key: i)) { |
486 | const auto &conf = m_pointsConfiguration[i]; |
487 | if (conf.contains(key: QXYSeries::PointConfiguration::Visibility)) { |
488 | drawPoint = |
489 | m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility] |
490 | .toBool(); |
491 | } |
492 | |
493 | if (drawPoint) { |
494 | if (conf.contains(key: QXYSeries::PointConfiguration::Size)) { |
495 | ptSize = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Size] |
496 | .toReal(); |
497 | } |
498 | |
499 | if (conf.contains(key: QXYSeries::PointConfiguration::Color)) { |
500 | painter->setBrush( |
501 | m_pointsConfiguration[i][QXYSeries::PointConfiguration::Color] |
502 | .value<QColor>()); |
503 | } |
504 | } |
505 | } |
506 | |
507 | if (m_series->isPointSelected(index: i)) { |
508 | // Selected points are drawn regardless of m_pointsVisible settings and |
509 | // custom point configuration. However, they are not drawn if light markers |
510 | // are used. The reason of this is to avoid displaying selected point |
511 | // over selected light marker. |
512 | drawPoint = m_series->selectedLightMarker().isNull(); |
513 | ptSize = ptSize * 1.5; |
514 | if (m_selectedColor.isValid()) |
515 | painter->setBrush(m_selectedColor); |
516 | } |
517 | |
518 | if (drawPoint) |
519 | painter->drawEllipse(center: m_linePoints.at(i), rx: ptSize, ry: ptSize); |
520 | |
521 | painter->restore(); |
522 | } |
523 | } |
524 | } |
525 | painter->restore(); |
526 | } |
527 | |
528 | void LineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event) |
529 | { |
530 | QPointF matchedP = matchForLightMarker(eventPos: event->pos()); |
531 | if (!qIsNaN(d: matchedP.x())) |
532 | emit XYChart::pressed(point: matchedP); |
533 | else |
534 | emit XYChart::pressed(point: domain()->calculateDomainPoint(point: event->pos())); |
535 | |
536 | m_lastMousePos = event->pos(); |
537 | m_mousePressed = true; |
538 | QGraphicsItem::mousePressEvent(event); |
539 | } |
540 | |
541 | void LineChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) |
542 | { |
543 | // Identical code in SplineChartItem |
544 | const QPointF matchedP = hoverPoint(eventPos: event->pos()); |
545 | m_lastHoveredMatchedPos = matchedP; |
546 | emit XYChart::hovered(point: matchedP, state: true); |
547 | |
548 | // event->accept(); |
549 | QGraphicsItem::hoverEnterEvent(event); |
550 | } |
551 | |
552 | void LineChartItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) |
553 | { |
554 | const QPointF matchedP = hoverPoint(eventPos: event->pos()); |
555 | if (!fuzzyComparePointF(p1: matchedP, p2: m_lastHoveredMatchedPos)) { |
556 | emit XYChart::hovered(point: matchedP, state: true); |
557 | m_lastHoveredMatchedPos = matchedP; |
558 | } |
559 | |
560 | QGraphicsItem::hoverMoveEvent(event); |
561 | } |
562 | |
563 | void LineChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) |
564 | { |
565 | const QPointF matchedP = hoverPoint(eventPos: event->pos()); |
566 | emit XYChart::hovered(point: matchedP, state: false); |
567 | m_lastHoveredMatchedPos = {qQNaN(), qQNaN()}; |
568 | |
569 | // event->accept(); |
570 | QGraphicsItem::hoverEnterEvent(event); |
571 | } |
572 | |
573 | void LineChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) |
574 | { |
575 | QPointF result; |
576 | QPointF matchedP = matchForLightMarker(eventPos: m_lastMousePos); |
577 | if (!qIsNaN(d: matchedP.x())) |
578 | result = matchedP; |
579 | else |
580 | result = domain()->calculateDomainPoint(point: m_lastMousePos); |
581 | |
582 | emit XYChart::released(point: result); |
583 | if (m_mousePressed) |
584 | emit XYChart::clicked(point: result); |
585 | m_mousePressed = false; |
586 | QGraphicsItem::mouseReleaseEvent(event); |
587 | } |
588 | |
589 | void LineChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) |
590 | { |
591 | QPointF matchedP = matchForLightMarker(eventPos: event->pos()); |
592 | if (!qIsNaN(d: matchedP.x())) |
593 | emit XYChart::doubleClicked(point: matchedP); |
594 | else |
595 | emit XYChart::doubleClicked(point: domain()->calculateDomainPoint(point: m_lastMousePos)); |
596 | |
597 | QGraphicsItem::mouseDoubleClickEvent(event); |
598 | } |
599 | |
600 | QT_END_NAMESPACE |
601 | |
602 | #include "moc_linechartitem_p.cpp" |
603 | |