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_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 &&currentGeometryPoint.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
335void 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
378void 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
528void 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
541void 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
552void 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
563void 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
573void 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
589void 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
600QT_END_NAMESPACE
601
602#include "moc_linechartitem_p.cpp"
603

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