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