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

source code of qtcharts/src/charts/linechart/linechartitem.cpp