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_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
59QRectF LineChartItem::boundingRect() const
60{
61 return m_rect;
62}
63
64QPainterPath LineChartItem::shape() const
65{
66 return m_shapePath;
67}
68
69void 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 &&currentGeometryPoint.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
334void 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
377void 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
527void 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
540void 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
552void 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
564void 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
580void 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
591QT_END_NAMESPACE
592
593#include "moc_linechartitem_p.cpp"
594

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