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