| 1 | /**************************************************************************** | 
| 2 | ** | 
| 3 | ** Copyright (C) 2016 The Qt Company Ltd. | 
| 4 | ** Contact: https://www.qt.io/licensing/ | 
| 5 | ** | 
| 6 | ** This file is part of the Qt Charts module of the Qt Toolkit. | 
| 7 | ** | 
| 8 | ** $QT_BEGIN_LICENSE:GPL$ | 
| 9 | ** Commercial License Usage | 
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in | 
| 11 | ** accordance with the commercial license agreement provided with the | 
| 12 | ** Software or, alternatively, in accordance with the terms contained in | 
| 13 | ** a written agreement between you and The Qt Company. For licensing terms | 
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further | 
| 15 | ** information use the contact form at https://www.qt.io/contact-us. | 
| 16 | ** | 
| 17 | ** GNU General Public License Usage | 
| 18 | ** Alternatively, this file may be used under the terms of the GNU | 
| 19 | ** General Public License version 3 or (at your option) any later version | 
| 20 | ** approved by the KDE Free Qt Foundation. The licenses are as published by | 
| 21 | ** the Free Software Foundation and appearing in the file LICENSE.GPL3 | 
| 22 | ** included in the packaging of this file. Please review the following | 
| 23 | ** information to ensure the GNU General Public License requirements will | 
| 24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. | 
| 25 | ** | 
| 26 | ** $QT_END_LICENSE$ | 
| 27 | ** | 
| 28 | ****************************************************************************/ | 
| 29 |  | 
| 30 | #include <private/linechartitem_p.h> | 
| 31 | #include <QtCharts/QLineSeries> | 
| 32 | #include <private/qlineseries_p.h> | 
| 33 | #include <private/chartpresenter_p.h> | 
| 34 | #include <private/polardomain_p.h> | 
| 35 | #include <private/chartthememanager_p.h> | 
| 36 | #include <private/charttheme_p.h> | 
| 37 | #include <QtGui/QPainter> | 
| 38 | #include <QtWidgets/QGraphicsSceneMouseEvent> | 
| 39 |  | 
| 40 | QT_CHARTS_BEGIN_NAMESPACE | 
| 41 |  | 
| 42 | LineChartItem::LineChartItem(QLineSeries *series, QGraphicsItem *item) | 
| 43 |     : XYChart(series,item), | 
| 44 |       m_series(series), | 
| 45 |       m_pointsVisible(false), | 
| 46 |       m_chartType(QChart::ChartTypeUndefined), | 
| 47 |       m_pointLabelsVisible(false), | 
| 48 |       m_pointLabelsFormat(series->pointLabelsFormat()), | 
| 49 |       m_pointLabelsFont(series->pointLabelsFont()), | 
| 50 |       m_pointLabelsColor(series->pointLabelsColor()), | 
| 51 |       m_pointLabelsClipping(true), | 
| 52 |       m_mousePressed(false) | 
| 53 | { | 
| 54 |     setAcceptHoverEvents(true); | 
| 55 |     setFlag(flag: QGraphicsItem::ItemIsSelectable); | 
| 56 |     setZValue(ChartPresenter::LineChartZValue); | 
| 57 |     QObject::connect(sender: series->d_func(), SIGNAL(updated()), receiver: this, SLOT(handleUpdated())); | 
| 58 |     QObject::connect(sender: series, SIGNAL(visibleChanged()), receiver: this, SLOT(handleUpdated())); | 
| 59 |     QObject::connect(sender: series, SIGNAL(opacityChanged()), receiver: this, SLOT(handleUpdated())); | 
| 60 |     QObject::connect(sender: series, SIGNAL(pointLabelsFormatChanged(QString)), | 
| 61 |                      receiver: this, SLOT(handleUpdated())); | 
| 62 |     QObject::connect(sender: series, SIGNAL(pointLabelsVisibilityChanged(bool)), | 
| 63 |                      receiver: this, SLOT(handleUpdated())); | 
| 64 |     QObject::connect(sender: series, SIGNAL(pointLabelsFontChanged(QFont)), receiver: this, SLOT(handleUpdated())); | 
| 65 |     QObject::connect(sender: series, SIGNAL(pointLabelsColorChanged(QColor)), receiver: this, SLOT(handleUpdated())); | 
| 66 |     QObject::connect(sender: series, SIGNAL(pointLabelsClippingChanged(bool)), receiver: this, SLOT(handleUpdated())); | 
| 67 |     handleUpdated(); | 
| 68 | } | 
| 69 |  | 
| 70 | QRectF LineChartItem::boundingRect() const | 
| 71 | { | 
| 72 |     return m_rect; | 
| 73 | } | 
| 74 |  | 
| 75 | QPainterPath LineChartItem::shape() const | 
| 76 | { | 
| 77 |     return m_shapePath; | 
| 78 | } | 
| 79 |  | 
| 80 | void LineChartItem::updateGeometry() | 
| 81 | { | 
| 82 |     if (m_series->useOpenGL()) { | 
| 83 |         if (!m_rect.isEmpty()) { | 
| 84 |             prepareGeometryChange(); | 
| 85 |             // Changed signal seems to trigger even with empty region | 
| 86 |             m_rect = QRectF(); | 
| 87 |         } | 
| 88 |         update(); | 
| 89 |         return; | 
| 90 |     } | 
| 91 |  | 
| 92 |     // Store the points to a local variable so that the old line gets properly cleared | 
| 93 |     // when animation starts. | 
| 94 |     m_linePoints = geometryPoints(); | 
| 95 |     const QVector<QPointF> &points = m_linePoints; | 
| 96 |  | 
| 97 |     if (points.size() == 0) { | 
| 98 |         prepareGeometryChange(); | 
| 99 |         m_fullPath = QPainterPath(); | 
| 100 |         m_linePath = QPainterPath(); | 
| 101 |         m_rect = QRect(); | 
| 102 |         return; | 
| 103 |     } | 
| 104 |  | 
| 105 |     QPainterPath linePath; | 
| 106 |     QPainterPath fullPath; | 
| 107 |     // Use worst case scenario to determine required margin. | 
| 108 |     qreal margin = m_linePen.width() * 1.42; | 
| 109 |  | 
| 110 |     // Area series use component line series that aren't necessarily added to the chart themselves, | 
| 111 |     // so check if chart type is forced before trying to obtain it from the chart. | 
| 112 |     QChart::ChartType chartType = m_chartType; | 
| 113 |     if (chartType == QChart::ChartTypeUndefined) | 
| 114 |         chartType = m_series->chart()->chartType(); | 
| 115 |  | 
| 116 |     // For polar charts, we need special handling for angular (horizontal) | 
| 117 |     // points that are off-grid. | 
| 118 |     if (chartType == QChart::ChartTypePolar) { | 
| 119 |         QPainterPath linePathLeft; | 
| 120 |         QPainterPath linePathRight; | 
| 121 |         QPainterPath *currentSegmentPath = 0; | 
| 122 |         QPainterPath *previousSegmentPath = 0; | 
| 123 |         qreal minX = domain()->minX(); | 
| 124 |         qreal maxX = domain()->maxX(); | 
| 125 |         qreal minY = domain()->minY(); | 
| 126 |         QPointF currentSeriesPoint = m_series->at(index: 0); | 
| 127 |         QPointF currentGeometryPoint = points.at(i: 0); | 
| 128 |         QPointF previousGeometryPoint = points.at(i: 0); | 
| 129 |         int size = m_linePen.width(); | 
| 130 |         bool pointOffGrid = false; | 
| 131 |         bool previousPointWasOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); | 
| 132 |  | 
| 133 |         qreal domainRadius = domain()->size().height() / 2.0; | 
| 134 |         const QPointF centerPoint(domainRadius, domainRadius); | 
| 135 |  | 
| 136 |         if (!previousPointWasOffGrid) { | 
| 137 |             fullPath.moveTo(p: points.at(i: 0)); | 
| 138 |             if (m_pointsVisible && currentSeriesPoint.y() >= minY) { | 
| 139 |                 // Do not draw ellipses for points below minimum Y. | 
| 140 |                 linePath.addEllipse(center: points.at(i: 0), rx: size, ry: size); | 
| 141 |                 fullPath.addEllipse(center: points.at(i: 0), rx: size, ry: size); | 
| 142 |                 linePath.moveTo(p: points.at(i: 0)); | 
| 143 |                 fullPath.moveTo(p: points.at(i: 0)); | 
| 144 |             } | 
| 145 |         } | 
| 146 |  | 
| 147 |         qreal leftMarginLine = centerPoint.x() - margin; | 
| 148 |         qreal rightMarginLine = centerPoint.x() + margin; | 
| 149 |         qreal horizontal = centerPoint.y(); | 
| 150 |  | 
| 151 |         // See ScatterChartItem::updateGeometry() for explanation why seriesLastIndex is needed | 
| 152 |         const int seriesLastIndex = m_series->count() - 1; | 
| 153 |  | 
| 154 |         for (int i = 1; i < points.size(); i++) { | 
| 155 |             // Interpolating line fragments would be ugly when thick pen is used, | 
| 156 |             // so we work around it by utilizing three separate | 
| 157 |             // paths for line segments and clip those with custom regions at paint time. | 
| 158 |             // "Right" path contains segments that cross the axis line with visible point on the | 
| 159 |             // right side of the axis line, as well as segments that have one point within the margin | 
| 160 |             // on the right side of the axis line and another point on the right side of the chart. | 
| 161 |             // "Left" path contains points with similarly on the left side. | 
| 162 |             // "Full" path contains rest of the points. | 
| 163 |             // This doesn't yield perfect results always. E.g. when segment covers more than 90 | 
| 164 |             // degrees and both of the points are within the margin, one in the top half and one in the | 
| 165 |             // bottom half of the chart, the bottom one gets clipped incorrectly. | 
| 166 |             // However, this should be rare occurrence in any sensible chart. | 
| 167 |             currentSeriesPoint = m_series->at(index: qMin(a: seriesLastIndex, b: i)); | 
| 168 |             currentGeometryPoint = points.at(i); | 
| 169 |             pointOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); | 
| 170 |  | 
| 171 |             // Draw something unless both off-grid | 
| 172 |             if (!pointOffGrid || !previousPointWasOffGrid) { | 
| 173 |                 QPointF intersectionPoint; | 
| 174 |                 qreal y; | 
| 175 |                 if (pointOffGrid != previousPointWasOffGrid) { | 
| 176 |                     if (currentGeometryPoint.x() == previousGeometryPoint.x()) { | 
| 177 |                         y = currentGeometryPoint.y() + (currentGeometryPoint.y() - previousGeometryPoint.y()) / 2.0; | 
| 178 |                     } else { | 
| 179 |                         qreal ratio = (centerPoint.x() - currentGeometryPoint.x()) / (currentGeometryPoint.x() - previousGeometryPoint.x()); | 
| 180 |                         y = currentGeometryPoint.y() + (currentGeometryPoint.y() - previousGeometryPoint.y()) * ratio; | 
| 181 |                     } | 
| 182 |                     intersectionPoint = QPointF(centerPoint.x(), y); | 
| 183 |                 } | 
| 184 |  | 
| 185 |                 bool dummyOk; // We know points are ok, but this is needed | 
| 186 |                 qreal currentAngle = 0; | 
| 187 |                 qreal previousAngle = 0; | 
| 188 |                 if (const PolarDomain *pd = qobject_cast<const PolarDomain *>(object: domain())) { | 
| 189 |                     currentAngle = pd->toAngularCoordinate(value: currentSeriesPoint.x(), ok&: dummyOk); | 
| 190 |                     previousAngle = pd->toAngularCoordinate(value: m_series->at(index: i - 1).x(), ok&: dummyOk); | 
| 191 |                 } else { | 
| 192 |                     qWarning() << Q_FUNC_INFO << "Unexpected domain: "  << domain(); | 
| 193 |                 } | 
| 194 |                 if ((qAbs(t: currentAngle - previousAngle) > 180.0)) { | 
| 195 |                     // If the angle between two points is over 180 degrees (half X range), | 
| 196 |                     // any direct segment between them becomes meaningless. | 
| 197 |                     // In this case two line segments are drawn instead, from previous | 
| 198 |                     // point to the center and from center to current point. | 
| 199 |                     if ((previousAngle < 0.0 || (previousAngle <= 180.0 && previousGeometryPoint.x() < rightMarginLine)) | 
| 200 |                         && previousGeometryPoint.y() < horizontal) { | 
| 201 |                         currentSegmentPath = &linePathRight; | 
| 202 |                     } else if ((previousAngle > 360.0 || (previousAngle > 180.0 && previousGeometryPoint.x() > leftMarginLine)) | 
| 203 |                                 && previousGeometryPoint.y() < horizontal) { | 
| 204 |                         currentSegmentPath = &linePathLeft; | 
| 205 |                     } else if (previousAngle > 0.0 && previousAngle < 360.0) { | 
| 206 |                         currentSegmentPath = &linePath; | 
| 207 |                     } else { | 
| 208 |                         currentSegmentPath = 0; | 
| 209 |                     } | 
| 210 |  | 
| 211 |                     if (currentSegmentPath) { | 
| 212 |                         if (previousSegmentPath != currentSegmentPath) | 
| 213 |                             currentSegmentPath->moveTo(p: previousGeometryPoint); | 
| 214 |                         if (previousPointWasOffGrid) | 
| 215 |                             fullPath.moveTo(p: intersectionPoint); | 
| 216 |  | 
| 217 |                         currentSegmentPath->lineTo(p: centerPoint); | 
| 218 |                         fullPath.lineTo(p: centerPoint); | 
| 219 |                     } | 
| 220 |  | 
| 221 |                     previousSegmentPath = currentSegmentPath; | 
| 222 |  | 
| 223 |                     if ((currentAngle < 0.0 || (currentAngle <= 180.0 && currentGeometryPoint.x() < rightMarginLine)) | 
| 224 |                         && currentGeometryPoint.y() < horizontal) { | 
| 225 |                         currentSegmentPath = &linePathRight; | 
| 226 |                     } else if ((currentAngle > 360.0 || (currentAngle > 180.0 &¤tGeometryPoint.x() > leftMarginLine)) | 
| 227 |                                 && currentGeometryPoint.y() < horizontal) { | 
| 228 |                         currentSegmentPath = &linePathLeft; | 
| 229 |                     } else if (currentAngle > 0.0 && currentAngle < 360.0) { | 
| 230 |                         currentSegmentPath = &linePath; | 
| 231 |                     } else { | 
| 232 |                         currentSegmentPath = 0; | 
| 233 |                     } | 
| 234 |  | 
| 235 |                     if (currentSegmentPath) { | 
| 236 |                         if (previousSegmentPath != currentSegmentPath) | 
| 237 |                             currentSegmentPath->moveTo(p: centerPoint); | 
| 238 |                         if (!previousSegmentPath) | 
| 239 |                             fullPath.moveTo(p: centerPoint); | 
| 240 |  | 
| 241 |                         currentSegmentPath->lineTo(p: currentGeometryPoint); | 
| 242 |                         if (pointOffGrid) | 
| 243 |                             fullPath.lineTo(p: intersectionPoint); | 
| 244 |                         else | 
| 245 |                             fullPath.lineTo(p: currentGeometryPoint); | 
| 246 |                     } | 
| 247 |                 } else { | 
| 248 |                     if (previousAngle < 0.0 || currentAngle < 0.0 | 
| 249 |                         || ((previousAngle <= 180.0 && currentAngle <= 180.0) | 
| 250 |                             && ((previousGeometryPoint.x() < rightMarginLine && previousGeometryPoint.y() < horizontal) | 
| 251 |                                 || (currentGeometryPoint.x() < rightMarginLine && currentGeometryPoint.y() < horizontal)))) { | 
| 252 |                         currentSegmentPath = &linePathRight; | 
| 253 |                     } else if (previousAngle > 360.0 || currentAngle > 360.0 | 
| 254 |                                || ((previousAngle > 180.0 && currentAngle > 180.0) | 
| 255 |                                    && ((previousGeometryPoint.x() > leftMarginLine && previousGeometryPoint.y() < horizontal) | 
| 256 |                                        || (currentGeometryPoint.x() > leftMarginLine && currentGeometryPoint.y() < horizontal)))) { | 
| 257 |                         currentSegmentPath = &linePathLeft; | 
| 258 |                     } else { | 
| 259 |                         currentSegmentPath = &linePath; | 
| 260 |                     } | 
| 261 |  | 
| 262 |                     if (currentSegmentPath != previousSegmentPath) | 
| 263 |                         currentSegmentPath->moveTo(p: previousGeometryPoint); | 
| 264 |                     if (previousPointWasOffGrid) | 
| 265 |                         fullPath.moveTo(p: intersectionPoint); | 
| 266 |  | 
| 267 |                     if (pointOffGrid) | 
| 268 |                         fullPath.lineTo(p: intersectionPoint); | 
| 269 |                     else | 
| 270 |                         fullPath.lineTo(p: currentGeometryPoint); | 
| 271 |                     currentSegmentPath->lineTo(p: currentGeometryPoint); | 
| 272 |                 } | 
| 273 |             } else { | 
| 274 |                 currentSegmentPath = 0; | 
| 275 |             } | 
| 276 |  | 
| 277 |             previousPointWasOffGrid = pointOffGrid; | 
| 278 |             if (m_pointsVisible && !pointOffGrid && currentSeriesPoint.y() >= minY) { | 
| 279 |                 linePath.addEllipse(center: points.at(i), rx: size, ry: size); | 
| 280 |                 fullPath.addEllipse(center: points.at(i), rx: size, ry: size); | 
| 281 |                 linePath.moveTo(p: points.at(i)); | 
| 282 |                 fullPath.moveTo(p: points.at(i)); | 
| 283 |             } | 
| 284 |             previousSegmentPath = currentSegmentPath; | 
| 285 |             previousGeometryPoint = currentGeometryPoint; | 
| 286 |         } | 
| 287 |         m_linePathPolarRight = linePathRight; | 
| 288 |         m_linePathPolarLeft = linePathLeft; | 
| 289 |         // Note: This construction of m_fullpath is not perfect. The partial segments that are | 
| 290 |         // outside left/right clip regions at axis boundary still generate hover/click events, | 
| 291 |         // because shape doesn't get clipped. It doesn't seem possible to do sensibly. | 
| 292 |     } else { // not polar | 
| 293 |         linePath.moveTo(p: points.at(i: 0)); | 
| 294 |         for (int i = 1; i < points.size(); i++) | 
| 295 |             linePath.lineTo(p: points.at(i)); | 
| 296 |         fullPath = linePath; | 
| 297 |     } | 
| 298 |  | 
| 299 |     QPainterPathStroker stroker; | 
| 300 |     // QPainter::drawLine does not respect join styles, for example BevelJoin becomes MiterJoin. | 
| 301 |     // This is why we are prepared for the "worst case" scenario, i.e. use always MiterJoin and | 
| 302 |     // multiply line width with square root of two when defining shape and bounding rectangle. | 
| 303 |     stroker.setWidth(margin); | 
| 304 |     stroker.setJoinStyle(Qt::MiterJoin); | 
| 305 |     stroker.setCapStyle(Qt::SquareCap); | 
| 306 |     stroker.setMiterLimit(m_linePen.miterLimit()); | 
| 307 |  | 
| 308 |     QPainterPath checkShapePath = stroker.createStroke(path: fullPath); | 
| 309 |  | 
| 310 |     // Only zoom in if the bounding rects of the paths fit inside int limits. QWidget::update() uses | 
| 311 |     // a region that has to be compatible with QRect. | 
| 312 |     if (checkShapePath.boundingRect().height() <= INT_MAX | 
| 313 |             && checkShapePath.boundingRect().width() <= INT_MAX | 
| 314 |             && linePath.boundingRect().height() <= INT_MAX | 
| 315 |             && linePath.boundingRect().width() <= INT_MAX | 
| 316 |             && fullPath.boundingRect().height() <= INT_MAX | 
| 317 |             && fullPath.boundingRect().width() <= INT_MAX) { | 
| 318 |         prepareGeometryChange(); | 
| 319 |  | 
| 320 |         m_linePath = linePath; | 
| 321 |         m_fullPath = fullPath; | 
| 322 |         m_shapePath = checkShapePath; | 
| 323 |  | 
| 324 |         m_rect = m_shapePath.boundingRect(); | 
| 325 |     } else { | 
| 326 |         update(); | 
| 327 |     } | 
| 328 | } | 
| 329 |  | 
| 330 | void LineChartItem::handleUpdated() | 
| 331 | { | 
| 332 |     // If points visibility has changed, a geometry update is needed. | 
| 333 |     // Also, if pen changes when points are visible, geometry update is needed. | 
| 334 |     bool doGeometryUpdate = | 
| 335 |         (m_pointsVisible != m_series->pointsVisible()) | 
| 336 |         || (m_series->pointsVisible() && (m_linePen != m_series->pen())); | 
| 337 |     bool visibleChanged = m_series->isVisible() != isVisible(); | 
| 338 |     setVisible(m_series->isVisible()); | 
| 339 |     setOpacity(m_series->opacity()); | 
| 340 |     m_pointsVisible = m_series->pointsVisible(); | 
| 341 |     m_linePen = m_series->pen(); | 
| 342 |     m_pointLabelsFormat = m_series->pointLabelsFormat(); | 
| 343 |     m_pointLabelsVisible = m_series->pointLabelsVisible(); | 
| 344 |     m_pointLabelsFont = m_series->pointLabelsFont(); | 
| 345 |     m_pointLabelsColor = m_series->pointLabelsColor(); | 
| 346 |     bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping(); | 
| 347 |     m_pointLabelsClipping = m_series->pointLabelsClipping(); | 
| 348 |     if (doGeometryUpdate) | 
| 349 |         updateGeometry(); | 
| 350 |     else if (m_series->useOpenGL() && visibleChanged) | 
| 351 |         refreshGlChart(); | 
| 352 |  | 
| 353 |     // Update whole chart in case label clipping changed as labels can be outside series area | 
| 354 |     if (labelClippingChanged) | 
| 355 |         m_series->chart()->update(); | 
| 356 |     else | 
| 357 |         update(); | 
| 358 | } | 
| 359 |  | 
| 360 | void LineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) | 
| 361 | { | 
| 362 |     Q_UNUSED(widget) | 
| 363 |     Q_UNUSED(option) | 
| 364 |  | 
| 365 |     if (m_series->useOpenGL()) | 
| 366 |         return; | 
| 367 |  | 
| 368 |     QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); | 
| 369 |     // Adjust clip rect half a pixel in required dimensions to make it include lines along the | 
| 370 |     // plot area edges, but never increase clip so much that any portion of the line is draw beyond | 
| 371 |     // the plot area. | 
| 372 |     const qreal x1 = pos().x() - int(pos().x()); | 
| 373 |     const qreal y1 = pos().y() - int(pos().y()); | 
| 374 |     const qreal x2 = (clipRect.width() + 0.5) - int(clipRect.width() + 0.5); | 
| 375 |     const qreal y2 = (clipRect.height() + 0.5) - int(clipRect.height() + 0.5); | 
| 376 |     clipRect.adjust(xp1: -x1, yp1: -y1, xp2: qMax(a: x1, b: x2), yp2: qMax(a: y1, b: y2)); | 
| 377 |  | 
| 378 |     painter->save(); | 
| 379 |     painter->setPen(m_linePen); | 
| 380 |     bool alwaysUsePath = false; | 
| 381 |  | 
| 382 |     if (m_series->chart()->chartType() == QChart::ChartTypePolar) { | 
| 383 |         qreal halfWidth = domain()->size().width() / 2.0; | 
| 384 |         QRectF clipRectLeft = QRectF(0, 0, halfWidth, domain()->size().height()); | 
| 385 |         QRectF clipRectRight = QRectF(halfWidth, 0, halfWidth, domain()->size().height()); | 
| 386 |         QRegion fullPolarClipRegion(clipRect.toRect(), QRegion::Ellipse); | 
| 387 |         QRegion clipRegionLeft(fullPolarClipRegion.intersected(r: clipRectLeft.toRect())); | 
| 388 |         QRegion clipRegionRight(fullPolarClipRegion.intersected(r: clipRectRight.toRect())); | 
| 389 |         painter->setClipRegion(clipRegionLeft); | 
| 390 |         painter->drawPath(path: m_linePathPolarLeft); | 
| 391 |         painter->setClipRegion(clipRegionRight); | 
| 392 |         painter->drawPath(path: m_linePathPolarRight); | 
| 393 |         painter->setClipRegion(fullPolarClipRegion); | 
| 394 |         alwaysUsePath = true; // required for proper clipping | 
| 395 |     } else { | 
| 396 |         painter->setClipRect(clipRect); | 
| 397 |     } | 
| 398 |  | 
| 399 |     if (m_linePen.style() != Qt::SolidLine || alwaysUsePath) { | 
| 400 |         // If pen style is not solid line, use path painting to ensure proper pattern continuity | 
| 401 |         painter->drawPath(path: m_linePath); | 
| 402 |     } else { | 
| 403 |         for (int i = 1; i < m_linePoints.size(); ++i) | 
| 404 |             painter->drawLine(p1: m_linePoints.at(i: i - 1), p2: m_linePoints.at(i)); | 
| 405 |     } | 
| 406 |  | 
| 407 |     if (m_pointLabelsVisible) { | 
| 408 |         if (m_pointLabelsClipping) | 
| 409 |             painter->setClipping(true); | 
| 410 |         else | 
| 411 |             painter->setClipping(false); | 
| 412 |         m_series->d_func()->drawSeriesPointLabels(painter, points: m_linePoints, offset: m_linePen.width() / 2); | 
| 413 |     } | 
| 414 |  | 
| 415 |     painter->restore(); | 
| 416 |  | 
| 417 |     if (m_pointsVisible) { | 
| 418 |         // draw points that lie inside clipRect only | 
| 419 |         qreal ptSize = m_linePen.width() * 1.5; | 
| 420 |         painter->setPen(Qt::NoPen); | 
| 421 |         painter->setBrush(m_linePen.color()); | 
| 422 |         for (int i = 0; i < m_linePoints.size(); ++i) { | 
| 423 |             if (clipRect.contains(p: m_linePoints.at(i))) | 
| 424 |                 painter->drawEllipse(center: m_linePoints.at(i), rx: ptSize, ry: ptSize); | 
| 425 |         } | 
| 426 |     } | 
| 427 | } | 
| 428 |  | 
| 429 | void LineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event) | 
| 430 | { | 
| 431 |     emit XYChart::pressed(point: domain()->calculateDomainPoint(point: event->pos())); | 
| 432 |     m_lastMousePos = event->pos(); | 
| 433 |     m_mousePressed = true; | 
| 434 |     QGraphicsItem::mousePressEvent(event); | 
| 435 | } | 
| 436 |  | 
| 437 | void LineChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) | 
| 438 | { | 
| 439 |     emit XYChart::hovered(point: domain()->calculateDomainPoint(point: event->pos()), state: true); | 
| 440 | //    event->accept(); | 
| 441 |     QGraphicsItem::hoverEnterEvent(event); | 
| 442 | } | 
| 443 |  | 
| 444 | void LineChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) | 
| 445 | { | 
| 446 |     emit XYChart::hovered(point: domain()->calculateDomainPoint(point: event->pos()), state: false); | 
| 447 | //    event->accept(); | 
| 448 |     QGraphicsItem::hoverEnterEvent(event); | 
| 449 | } | 
| 450 |  | 
| 451 | void LineChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) | 
| 452 | { | 
| 453 |     emit XYChart::released(point: domain()->calculateDomainPoint(point: m_lastMousePos)); | 
| 454 |     if (m_mousePressed) | 
| 455 |         emit XYChart::clicked(point: domain()->calculateDomainPoint(point: m_lastMousePos)); | 
| 456 |     m_mousePressed = false; | 
| 457 |     QGraphicsItem::mouseReleaseEvent(event); | 
| 458 | } | 
| 459 |  | 
| 460 | void LineChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) | 
| 461 | { | 
| 462 |     emit XYChart::doubleClicked(point: domain()->calculateDomainPoint(point: m_lastMousePos)); | 
| 463 |     QGraphicsItem::mouseDoubleClickEvent(event); | 
| 464 | } | 
| 465 |  | 
| 466 | QT_CHARTS_END_NAMESPACE | 
| 467 |  | 
| 468 | #include "moc_linechartitem_p.cpp" | 
| 469 |  |