| 1 | // Copyright (C) 2021 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only | 
| 3 |  | 
| 4 | #include <private/splinechartitem_p.h> | 
| 5 | #include <private/qsplineseries_p.h> | 
| 6 | #include <private/chartpresenter_p.h> | 
| 7 | #include <private/splineanimation_p.h> | 
| 8 | #include <private/polardomain_p.h> | 
| 9 | #include <QtGui/QPainter> | 
| 10 | #include <QtWidgets/QGraphicsSceneMouseEvent> | 
| 11 |  | 
| 12 | QT_BEGIN_NAMESPACE | 
| 13 |  | 
| 14 | SplineChartItem::SplineChartItem(QSplineSeries *series, QGraphicsItem *item) | 
| 15 |     : XYChart(series,item), | 
| 16 |       m_series(series), | 
| 17 |       m_pointsVisible(false), | 
| 18 |       m_animation(0), | 
| 19 |       m_pointLabelsVisible(false), | 
| 20 |       m_markerSize(series->markerSize()), | 
| 21 |       m_pointLabelsFormat(series->pointLabelsFormat()), | 
| 22 |       m_pointLabelsFont(series->pointLabelsFont()), | 
| 23 |       m_pointLabelsColor(series->pointLabelsColor()), | 
| 24 |       m_pointLabelsClipping(true), | 
| 25 |       m_lastHoveredMatchedPos{qQNaN(), qQNaN()}, | 
| 26 |       m_mousePressed(false) | 
| 27 | { | 
| 28 |     setAcceptHoverEvents(true); | 
| 29 |     setFlag(flag: QGraphicsItem::ItemIsSelectable); | 
| 30 |     setZValue(ChartPresenter::SplineChartZValue); | 
| 31 |     connect(sender: m_series->d_func(), signal: &QXYSeriesPrivate::seriesUpdated, | 
| 32 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 33 |     connect(sender: series, signal: &QXYSeries::lightMarkerChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 34 |     connect(sender: series, signal: &QXYSeries::selectedLightMarkerChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 35 |     connect(sender: series, signal: &QXYSeries::markerSizeChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 36 |     connect(sender: series, signal: &QXYSeries::visibleChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 37 |     connect(sender: series, signal: &QXYSeries::opacityChanged, context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 38 |     connect(sender: series, signal: &QXYSeries::pointLabelsFormatChanged, | 
| 39 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 40 |     connect(sender: series, signal: &QXYSeries::pointLabelsVisibilityChanged, | 
| 41 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 42 |     connect(sender: series, signal: &QXYSeries::pointLabelsFontChanged, | 
| 43 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 44 |     connect(sender: series, signal: &QXYSeries::pointLabelsColorChanged, | 
| 45 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 46 |     connect(sender: series, signal: &QXYSeries::pointLabelsClippingChanged, | 
| 47 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 48 |     connect(sender: series, signal: &QSplineSeries::selectedColorChanged, | 
| 49 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 50 |     connect(sender: series, signal: &QLineSeries::selectedPointsChanged, | 
| 51 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 52 |     connect(sender: series, signal: &QSplineSeries::pointsConfigurationChanged, | 
| 53 |             context: this, slot: &SplineChartItem::handleSeriesUpdated); | 
| 54 |  | 
| 55 |     handleSeriesUpdated(); | 
| 56 | } | 
| 57 |  | 
| 58 | QRectF SplineChartItem::boundingRect() const | 
| 59 | { | 
| 60 |     return m_rect; | 
| 61 | } | 
| 62 |  | 
| 63 | QPainterPath SplineChartItem::shape() const | 
| 64 | { | 
| 65 |     return m_fullPath; | 
| 66 | } | 
| 67 |  | 
| 68 | void SplineChartItem::setAnimation(SplineAnimation *animation) | 
| 69 | { | 
| 70 |     m_animation = animation; | 
| 71 |     XYChart::setAnimation(animation); | 
| 72 | } | 
| 73 |  | 
| 74 | ChartAnimation *SplineChartItem::animation() const | 
| 75 | { | 
| 76 |     return m_animation; | 
| 77 | } | 
| 78 |  | 
| 79 | void SplineChartItem::setControlGeometryPoints(const QList<QPointF> &points) | 
| 80 | { | 
| 81 |     m_controlPoints = points; | 
| 82 | } | 
| 83 |  | 
| 84 | QList<QPointF> SplineChartItem::controlGeometryPoints() const | 
| 85 | { | 
| 86 |     return m_controlPoints; | 
| 87 | } | 
| 88 |  | 
| 89 | void SplineChartItem::updateChart(const QList<QPointF> &oldPoints, const QList<QPointF> &newPoints, | 
| 90 |                                   int index) | 
| 91 | { | 
| 92 |     QList<QPointF> controlPoints; | 
| 93 |     if (newPoints.size() >= 2) | 
| 94 |         controlPoints = calculateControlPoints(points: newPoints); | 
| 95 |  | 
| 96 |     if (m_animation) | 
| 97 |         m_animation->setup(oldPoints, newPoints, oldContorlPoints: m_controlPoints, newControlPoints: controlPoints, index); | 
| 98 |  | 
| 99 |     m_points = newPoints; | 
| 100 |     m_controlPoints = controlPoints; | 
| 101 |     setDirty(false); | 
| 102 |  | 
| 103 |     if (m_animation) | 
| 104 |         presenter()->startAnimation(animation: m_animation); | 
| 105 |     else | 
| 106 |         updateGeometry(); | 
| 107 | } | 
| 108 |  | 
| 109 | void SplineChartItem::updateGeometry() | 
| 110 | { | 
| 111 |     const QList<QPointF> &points = m_points; | 
| 112 |     const QList<QPointF> &controlPoints = m_controlPoints; | 
| 113 |  | 
| 114 |     if ((points.size() < 2) || (controlPoints.size() < 2)) { | 
| 115 |         prepareGeometryChange(); | 
| 116 |         m_path = QPainterPath(); | 
| 117 |         m_rect = QRect(); | 
| 118 |         return; | 
| 119 |     } | 
| 120 |  | 
| 121 |     Q_ASSERT(points.size() * 2 - 2 == controlPoints.size()); | 
| 122 |  | 
| 123 |     QPainterPath splinePath; | 
| 124 |     QPainterPath fullPath; | 
| 125 |     // Use worst case scenario to determine required margin. | 
| 126 |     qreal margin = m_linePen.width() * 1.42; | 
| 127 |  | 
| 128 |     if (m_series->chart()->chartType() == QChart::ChartTypePolar) { | 
| 129 |         QPainterPath splinePathLeft; | 
| 130 |         QPainterPath splinePathRight; | 
| 131 |         QPainterPath *currentSegmentPath = 0; | 
| 132 |         QPainterPath *previousSegmentPath = 0; | 
| 133 |         qreal minX = domain()->minX(); | 
| 134 |         qreal maxX = domain()->maxX(); | 
| 135 |         qreal minY = domain()->minY(); | 
| 136 |         QPointF currentSeriesPoint = m_series->at(index: 0); | 
| 137 |         QPointF currentGeometryPoint = points.at(i: 0); | 
| 138 |         QPointF previousGeometryPoint = points.at(i: 0); | 
| 139 |         bool pointOffGrid = false; | 
| 140 |         bool previousPointWasOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); | 
| 141 |         m_visiblePoints.clear(); | 
| 142 |         m_visiblePoints.reserve(asize: points.size()); | 
| 143 |  | 
| 144 |         qreal domainRadius = domain()->size().height() / 2.0; | 
| 145 |         const QPointF centerPoint(domainRadius, domainRadius); | 
| 146 |  | 
| 147 |         if (!previousPointWasOffGrid) { | 
| 148 |             fullPath.moveTo(p: points.at(i: 0)); | 
| 149 |             // Do not draw points for points below minimum Y. | 
| 150 |             if (m_pointsVisible && currentSeriesPoint.y() >= minY) | 
| 151 |                 m_visiblePoints.append(t: currentGeometryPoint); | 
| 152 |         } | 
| 153 |  | 
| 154 |         qreal leftMarginLine = centerPoint.x() - margin; | 
| 155 |         qreal rightMarginLine = centerPoint.x() + margin; | 
| 156 |         qreal horizontal = centerPoint.y(); | 
| 157 |  | 
| 158 |         // See ScatterChartItem::updateGeometry() for explanation why seriesLastIndex is needed | 
| 159 |         const int seriesLastIndex = m_series->count() - 1; | 
| 160 |  | 
| 161 |         for (int i = 1; i < points.size(); i++) { | 
| 162 |             // Interpolating spline fragments accurately is not trivial, and would anyway be ugly | 
| 163 |             // when thick pen is used, so we work around it by utilizing three separate | 
| 164 |             // paths for spline segments and clip those with custom regions at paint time. | 
| 165 |             // "Right" path contains segments that cross the axis line with visible point on the | 
| 166 |             // right side of the axis line, as well as segments that have one point within the margin | 
| 167 |             // on the right side of the axis line and another point on the right side of the chart. | 
| 168 |             // "Left" path contains points with similarly on the left side. | 
| 169 |             // "Full" path contains rest of the points. | 
| 170 |             // This doesn't yield perfect results always. E.g. when segment covers more than 90 | 
| 171 |             // degrees and both of the points are within the margin, one in the top half and one in the | 
| 172 |             // bottom half of the chart, the bottom one gets clipped incorrectly. | 
| 173 |             // However, this should be rare occurrence in any sensible chart. | 
| 174 |             currentSeriesPoint = m_series->at(index: qMin(a: seriesLastIndex, b: i)); | 
| 175 |             currentGeometryPoint = points.at(i); | 
| 176 |             pointOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); | 
| 177 |  | 
| 178 |             // Draw something unless both off-grid | 
| 179 |             if (!pointOffGrid || !previousPointWasOffGrid) { | 
| 180 |                 bool dummyOk; // We know points are ok, but this is needed | 
| 181 |                 qreal currentAngle = static_cast<PolarDomain *>(domain())->toAngularCoordinate(value: currentSeriesPoint.x(), ok&: dummyOk); | 
| 182 |                 qreal previousAngle = static_cast<PolarDomain *>(domain())->toAngularCoordinate(value: m_series->at(index: i - 1).x(), ok&: dummyOk); | 
| 183 |  | 
| 184 |                 if ((qAbs(t: currentAngle - previousAngle) > 180.0)) { | 
| 185 |                     // If the angle between two points is over 180 degrees (half X range), | 
| 186 |                     // any direct segment between them becomes meaningless. | 
| 187 |                     // In this case two line segments are drawn instead, from previous | 
| 188 |                     // point to the center and from center to current point. | 
| 189 |                     if ((previousAngle < 0.0 || (previousAngle <= 180.0 && previousGeometryPoint.x() < rightMarginLine)) | 
| 190 |                         && previousGeometryPoint.y() < horizontal) { | 
| 191 |                         currentSegmentPath = &splinePathRight; | 
| 192 |                     } else if ((previousAngle > 360.0 || (previousAngle > 180.0 && previousGeometryPoint.x() > leftMarginLine)) | 
| 193 |                                 && previousGeometryPoint.y() < horizontal) { | 
| 194 |                         currentSegmentPath = &splinePathLeft; | 
| 195 |                     } else if (previousAngle > 0.0 && previousAngle < 360.0) { | 
| 196 |                         currentSegmentPath = &splinePath; | 
| 197 |                     } else { | 
| 198 |                         currentSegmentPath = 0; | 
| 199 |                     } | 
| 200 |  | 
| 201 |                     if (currentSegmentPath) { | 
| 202 |                         if (previousSegmentPath != currentSegmentPath) | 
| 203 |                             currentSegmentPath->moveTo(p: previousGeometryPoint); | 
| 204 |                         if (!previousSegmentPath) | 
| 205 |                             fullPath.moveTo(p: previousGeometryPoint); | 
| 206 |  | 
| 207 |                         currentSegmentPath->lineTo(p: centerPoint); | 
| 208 |                         fullPath.lineTo(p: centerPoint); | 
| 209 |                     } | 
| 210 |  | 
| 211 |                     previousSegmentPath = currentSegmentPath; | 
| 212 |  | 
| 213 |                     if ((currentAngle < 0.0 || (currentAngle <= 180.0 && currentGeometryPoint.x() < rightMarginLine)) | 
| 214 |                         && currentGeometryPoint.y() < horizontal) { | 
| 215 |                         currentSegmentPath = &splinePathRight; | 
| 216 |                     } else if ((currentAngle > 360.0 || (currentAngle > 180.0 &¤tGeometryPoint.x() > leftMarginLine)) | 
| 217 |                                 && currentGeometryPoint.y() < horizontal) { | 
| 218 |                         currentSegmentPath = &splinePathLeft; | 
| 219 |                     } else if (currentAngle > 0.0 && currentAngle < 360.0) { | 
| 220 |                         currentSegmentPath = &splinePath; | 
| 221 |                     } else { | 
| 222 |                         currentSegmentPath = 0; | 
| 223 |                     } | 
| 224 |  | 
| 225 |                     if (currentSegmentPath) { | 
| 226 |                         if (previousSegmentPath != currentSegmentPath) | 
| 227 |                             currentSegmentPath->moveTo(p: centerPoint); | 
| 228 |                         if (!previousSegmentPath) | 
| 229 |                             fullPath.moveTo(p: centerPoint); | 
| 230 |  | 
| 231 |                         currentSegmentPath->lineTo(p: currentGeometryPoint); | 
| 232 |                         fullPath.lineTo(p: currentGeometryPoint); | 
| 233 |                     } | 
| 234 |                 } else { | 
| 235 |                     QPointF cp1 = controlPoints[2 * (i - 1)]; | 
| 236 |                     QPointF cp2 = controlPoints[(2 * i) - 1]; | 
| 237 |  | 
| 238 |                     if (previousAngle < 0.0 || currentAngle < 0.0 | 
| 239 |                         || ((previousAngle <= 180.0 && currentAngle <= 180.0) | 
| 240 |                             && ((previousGeometryPoint.x() < rightMarginLine && previousGeometryPoint.y() < horizontal) | 
| 241 |                                 || (currentGeometryPoint.x() < rightMarginLine && currentGeometryPoint.y() < horizontal)))) { | 
| 242 |                         currentSegmentPath = &splinePathRight; | 
| 243 |                     } else if (previousAngle > 360.0 || currentAngle > 360.0 | 
| 244 |                                || ((previousAngle > 180.0 && currentAngle > 180.0) | 
| 245 |                                    && ((previousGeometryPoint.x() > leftMarginLine && previousGeometryPoint.y() < horizontal) | 
| 246 |                                        || (currentGeometryPoint.x() > leftMarginLine && currentGeometryPoint.y() < horizontal)))) { | 
| 247 |                         currentSegmentPath = &splinePathLeft; | 
| 248 |                     } else { | 
| 249 |                         currentSegmentPath = &splinePath; | 
| 250 |                     } | 
| 251 |  | 
| 252 |                     if (currentSegmentPath != previousSegmentPath) | 
| 253 |                         currentSegmentPath->moveTo(p: previousGeometryPoint); | 
| 254 |                     if (!previousSegmentPath) | 
| 255 |                         fullPath.moveTo(p: previousGeometryPoint); | 
| 256 |  | 
| 257 |                     fullPath.cubicTo(ctrlPt1: cp1, ctrlPt2: cp2, endPt: currentGeometryPoint); | 
| 258 |                     currentSegmentPath->cubicTo(ctrlPt1: cp1, ctrlPt2: cp2, endPt: currentGeometryPoint); | 
| 259 |                 } | 
| 260 |             } else { | 
| 261 |                 currentSegmentPath = 0; | 
| 262 |             } | 
| 263 |  | 
| 264 |             previousPointWasOffGrid = pointOffGrid; | 
| 265 |             if (!pointOffGrid && m_pointsVisible && currentSeriesPoint.y() >= minY) | 
| 266 |                 m_visiblePoints.append(t: currentGeometryPoint); | 
| 267 |             previousSegmentPath = currentSegmentPath; | 
| 268 |             previousGeometryPoint = currentGeometryPoint; | 
| 269 |         } | 
| 270 |  | 
| 271 |         m_pathPolarRight = splinePathRight; | 
| 272 |         m_pathPolarLeft = splinePathLeft; | 
| 273 |         // Note: This construction of m_fullpath is not perfect. The partial segments that are | 
| 274 |         // outside left/right clip regions at axis boundary still generate hover/click events, | 
| 275 |         // because shape doesn't get clipped. It doesn't seem possible to do sensibly. | 
| 276 |     } else { // not polar | 
| 277 |         splinePath.moveTo(p: points.at(i: 0)); | 
| 278 |         for (int i = 0; i < points.size() - 1; i++) { | 
| 279 |             const QPointF &point = points.at(i: i + 1); | 
| 280 |             splinePath.cubicTo(ctrlPt1: controlPoints[2 * i], ctrlPt2: controlPoints[2 * i + 1], endPt: point); | 
| 281 |         } | 
| 282 |         fullPath = splinePath; | 
| 283 |     } | 
| 284 |  | 
| 285 |     QPainterPathStroker stroker; | 
| 286 |     // The full path is comprised of three separate paths. | 
| 287 |     // This is why we are prepared for the "worst case" scenario, i.e. use always MiterJoin and | 
| 288 |     // multiply line width with square root of two when defining shape and bounding rectangle. | 
| 289 |     stroker.setWidth(margin); | 
| 290 |     stroker.setJoinStyle(Qt::MiterJoin); | 
| 291 |     stroker.setCapStyle(Qt::SquareCap); | 
| 292 |     stroker.setMiterLimit(m_linePen.miterLimit()); | 
| 293 |  | 
| 294 |     // Only zoom in if the bounding rects of the path fit inside int limits. QWidget::update() uses | 
| 295 |     // a region that has to be compatible with QRect. | 
| 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() || (!m_series->selectedLightMarker().isNull() | 
| 301 |                                                  && !m_series->selectedPoints().isEmpty())) { | 
| 302 |         // +1, +2: a margin to guarantee we cover all of the pixmap | 
| 303 |         qreal markerHalfSize = (m_series->markerSize() / 2.0) + 1; | 
| 304 |         qreal markerSize = m_series->markerSize() + 2; | 
| 305 |  | 
| 306 |         for (const auto &point : std::as_const(t: points)) { | 
| 307 |             checkShapePath.addRect(x: point.x() - markerHalfSize, | 
| 308 |                                    y: point.y() - markerHalfSize, | 
| 309 |                                    w: markerSize, h: markerSize); | 
| 310 |         } | 
| 311 |     } | 
| 312 |  | 
| 313 |     if (checkShapePath.boundingRect().height() <= INT_MAX | 
| 314 |             && checkShapePath.boundingRect().width() <= INT_MAX | 
| 315 |             && splinePath.boundingRect().height() <= INT_MAX | 
| 316 |             && splinePath.boundingRect().width() <= INT_MAX) { | 
| 317 |         m_path = splinePath; | 
| 318 |  | 
| 319 |         prepareGeometryChange(); | 
| 320 |  | 
| 321 |         m_fullPath = checkShapePath; | 
| 322 |         m_rect = m_fullPath.boundingRect(); | 
| 323 |     } | 
| 324 | } | 
| 325 |  | 
| 326 | /*! | 
| 327 |   Calculates control points which are needed by QPainterPath.cubicTo function to draw the cubic Bezier cureve between two points. | 
| 328 |   */ | 
| 329 | QList<QPointF> SplineChartItem::calculateControlPoints(const QList<QPointF> &points) | 
| 330 | { | 
| 331 |     QList<QPointF> controlPoints; | 
| 332 |     controlPoints.resize(size: points.size() * 2 - 2); | 
| 333 |  | 
| 334 |     int n = points.size() - 1; | 
| 335 |  | 
| 336 |     if (n == 1) { | 
| 337 |         //for n==1 | 
| 338 |         controlPoints[0].setX((2 * points[0].x() + points[1].x()) / 3); | 
| 339 |         controlPoints[0].setY((2 * points[0].y() + points[1].y()) / 3); | 
| 340 |         controlPoints[1].setX(2 * controlPoints[0].x() - points[0].x()); | 
| 341 |         controlPoints[1].setY(2 * controlPoints[0].y() - points[0].y()); | 
| 342 |         return controlPoints; | 
| 343 |     } | 
| 344 |  | 
| 345 |     // Calculate first Bezier control points | 
| 346 |     // Set of equations for P0 to Pn points. | 
| 347 |     // | 
| 348 |     //  |   2   1   0   0   ... 0   0   0   ... 0   0   0   |   |   P1_1    |   |   P0 + 2 * P1             | | 
| 349 |     //  |   1   4   1   0   ... 0   0   0   ... 0   0   0   |   |   P1_2    |   |   4 * P1 + 2 * P2         | | 
| 350 |     //  |   0   1   4   1   ... 0   0   0   ... 0   0   0   |   |   P1_3    |   |   4 * P2 + 2 * P3         | | 
| 351 |     //  |   .   .   .   .   .   .   .   .   .   .   .   .   |   |   ...     |   |   ...                     | | 
| 352 |     //  |   0   0   0   0   ... 1   4   1   ... 0   0   0   | * |   P1_i    | = |   4 * P(i-1) + 2 * Pi     | | 
| 353 |     //  |   .   .   .   .   .   .   .   .   .   .   .   .   |   |   ...     |   |   ...                     | | 
| 354 |     //  |   0   0   0   0   0   0   0   0   ... 1   4   1   |   |   P1_(n-1)|   |   4 * P(n-2) + 2 * P(n-1) | | 
| 355 |     //  |   0   0   0   0   0   0   0   0   ... 0   2   7   |   |   P1_n    |   |   8 * P(n-1) + Pn         | | 
| 356 |     // | 
| 357 |     QList<qreal> list; | 
| 358 |     list.resize(size: n); | 
| 359 |  | 
| 360 |     list[0] = points[0].x() + 2 * points[1].x(); | 
| 361 |  | 
| 362 |     for (int i = 1; i < n - 1; ++i) | 
| 363 |         list[i] = 4 * points[i].x() + 2 * points[i + 1].x(); | 
| 364 |  | 
| 365 |     list[n - 1] = (8 * points[n - 1].x() + points[n].x()) / 2.0; | 
| 366 |  | 
| 367 |     const QList<qreal> xControl = firstControlPoints(list); | 
| 368 |  | 
| 369 |     list[0] = points[0].y() + 2 * points[1].y(); | 
| 370 |  | 
| 371 |     for (int i = 1; i < n - 1; ++i) | 
| 372 |         list[i] = 4 * points[i].y() + 2 * points[i + 1].y(); | 
| 373 |  | 
| 374 |     list[n - 1] = (8 * points[n - 1].y() + points[n].y()) / 2.0; | 
| 375 |  | 
| 376 |     const QList<qreal> yControl = firstControlPoints(list); | 
| 377 |  | 
| 378 |     for (int i = 0, j = 0; i < n; ++i, ++j) { | 
| 379 |  | 
| 380 |         controlPoints[j].setX(xControl[i]); | 
| 381 |         controlPoints[j].setY(yControl[i]); | 
| 382 |  | 
| 383 |         j++; | 
| 384 |  | 
| 385 |         if (i < n - 1) { | 
| 386 |             controlPoints[j].setX(2 * points[i + 1].x() - xControl[i + 1]); | 
| 387 |             controlPoints[j].setY(2 * points[i + 1].y() - yControl[i + 1]); | 
| 388 |         } else { | 
| 389 |             controlPoints[j].setX((points[n].x() + xControl[n - 1]) / 2); | 
| 390 |             controlPoints[j].setY((points[n].y() + yControl[n - 1]) / 2); | 
| 391 |         } | 
| 392 |     } | 
| 393 |     return controlPoints; | 
| 394 | } | 
| 395 |  | 
| 396 | QList<qreal> SplineChartItem::firstControlPoints(const QList<qreal> &list) | 
| 397 | { | 
| 398 |     QList<qreal> result; | 
| 399 |  | 
| 400 |     int count = list.size(); | 
| 401 |     result.resize(size: count); | 
| 402 |     result[0] = list[0] / 2.0; | 
| 403 |  | 
| 404 |     QList<qreal> temp; | 
| 405 |     temp.resize(size: count); | 
| 406 |     temp[0] = 0; | 
| 407 |  | 
| 408 |     qreal b = 2.0; | 
| 409 |  | 
| 410 |     for (int i = 1; i < count; i++) { | 
| 411 |         temp[i] = 1 / b; | 
| 412 |         b = (i < count - 1 ? 4.0 : 3.5) - temp[i]; | 
| 413 |         result[i] = (list[i] - result[i - 1]) / b; | 
| 414 |     } | 
| 415 |  | 
| 416 |     for (int i = 1; i < count; i++) | 
| 417 |         result[count - i - 1] -= temp[count - i] * result[count - i]; | 
| 418 |  | 
| 419 |     return result; | 
| 420 | } | 
| 421 |  | 
| 422 | //handlers | 
| 423 |  | 
| 424 | void SplineChartItem::handleSeriesUpdated() | 
| 425 | { | 
| 426 |     setVisible(m_series->isVisible()); | 
| 427 |     setOpacity(m_series->opacity()); | 
| 428 |     m_pointsVisible = m_series->pointsVisible(); | 
| 429 |     m_linePen = m_series->pen(); | 
| 430 |     m_pointPen = m_series->pen(); | 
| 431 |     m_pointPen.setWidthF(2 * m_pointPen.width()); | 
| 432 |     m_pointLabelsFormat = m_series->pointLabelsFormat(); | 
| 433 |     m_pointLabelsVisible = m_series->pointLabelsVisible(); | 
| 434 |     m_markerSize = m_series->markerSize(); | 
| 435 |     m_pointLabelsFont = m_series->pointLabelsFont(); | 
| 436 |     m_pointLabelsColor = m_series->pointLabelsColor(); | 
| 437 |     m_selectedPoints = m_series->selectedPoints(); | 
| 438 |     m_selectedColor = m_series->selectedColor(); | 
| 439 |     bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping(); | 
| 440 |     m_pointLabelsClipping = m_series->pointLabelsClipping(); | 
| 441 |     // Update whole chart in case label clipping changed as labels can be outside series area | 
| 442 |     if (labelClippingChanged) | 
| 443 |         m_series->chart()->update(); | 
| 444 |     else | 
| 445 |         update(); | 
| 446 | } | 
| 447 |  | 
| 448 | //painter | 
| 449 |  | 
| 450 | void SplineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) | 
| 451 | { | 
| 452 |     Q_UNUSED(widget); | 
| 453 |     Q_UNUSED(option); | 
| 454 |  | 
| 455 |     QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); | 
| 456 |  | 
| 457 |     painter->save(); | 
| 458 |     painter->setPen(m_linePen); | 
| 459 |     painter->setBrush(Qt::NoBrush); | 
| 460 |  | 
| 461 |     if (m_series->chart()->chartType() == QChart::ChartTypePolar) { | 
| 462 |         qreal halfWidth = domain()->size().width() / 2.; | 
| 463 |         QRectF clipRectLeft = QRectF(0, 0, halfWidth, domain()->size().height()); | 
| 464 |         QRectF clipRectRight = QRectF(halfWidth, 0, halfWidth, domain()->size().height()); | 
| 465 |         QRegion fullPolarClipRegion(clipRect.toRect(), QRegion::Ellipse); | 
| 466 |         QRegion clipRegionLeft(fullPolarClipRegion.intersected(r: clipRectLeft.toRect())); | 
| 467 |         QRegion clipRegionRight(fullPolarClipRegion.intersected(r: clipRectRight.toRect())); | 
| 468 |         painter->setClipRegion(clipRegionLeft); | 
| 469 |         painter->drawPath(path: m_pathPolarLeft); | 
| 470 |         painter->setClipRegion(clipRegionRight); | 
| 471 |         painter->drawPath(path: m_pathPolarRight); | 
| 472 |         painter->setClipRegion(fullPolarClipRegion); | 
| 473 |     } else { | 
| 474 |         painter->setClipRect(clipRect); | 
| 475 |     } | 
| 476 |  | 
| 477 |     if (m_series->bestFitLineVisible()) | 
| 478 |         m_series->d_func()->drawBestFitLine(painter, clipRect); | 
| 479 |  | 
| 480 |     painter->drawPath(path: m_path); | 
| 481 |  | 
| 482 |     int pointLabelsOffset = m_linePen.width() / 2; | 
| 483 |  | 
| 484 |     // Draw markers if a marker or marker for selected points only has been | 
| 485 |     // set (set to QImage() to disable) | 
| 486 |     if (!m_series->lightMarker().isNull() || (!m_series->selectedLightMarker().isNull() | 
| 487 |                                            && !m_series->selectedPoints().isEmpty())) { | 
| 488 |         const QImage &marker = m_series->lightMarker(); | 
| 489 |         const QImage &selectedMarker = m_series->selectedLightMarker(); | 
| 490 |         qreal markerHalfSize = m_markerSize / 2.0; | 
| 491 |         pointLabelsOffset = markerHalfSize; | 
| 492 |  | 
| 493 |         for (int i = 0; i < m_points.size(); ++i) { | 
| 494 |             // Documentation of light markers says that points visibility and | 
| 495 |             // light markers are independent features. Therefore m_pointsVisible | 
| 496 |             // is not used here as light markers are drawn if lightMarker is not null. | 
| 497 |             // However points visibility configuration can be still used here. | 
| 498 |             bool drawPoint = !m_series->lightMarker().isNull(); | 
| 499 |             if (m_pointsConfiguration.contains(key: i)) { | 
| 500 |                 const auto &conf = m_pointsConfiguration[i]; | 
| 501 |  | 
| 502 |                 if (conf.contains(key: QXYSeries::PointConfiguration::Visibility)) { | 
| 503 |                     drawPoint = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility] | 
| 504 |                                         .toBool(); | 
| 505 |                 } | 
| 506 |             } | 
| 507 |  | 
| 508 |             bool drawSelectedPoint = false; | 
| 509 |             if (m_series->isPointSelected(index: i)) { | 
| 510 |                 drawPoint = true; | 
| 511 |                 drawSelectedPoint = !selectedMarker.isNull(); | 
| 512 |             } | 
| 513 |             if (drawPoint) { | 
| 514 |                 const QRectF rect(m_points[i].x() - markerHalfSize, | 
| 515 |                                   m_points[i].y() - markerHalfSize, | 
| 516 |                                   m_markerSize, m_markerSize); | 
| 517 |                 painter->drawImage(r: rect, image: drawSelectedPoint ? selectedMarker : marker); | 
| 518 |             } | 
| 519 |         } | 
| 520 |     } | 
| 521 |  | 
| 522 |     if (m_pointLabelsVisible) { | 
| 523 |         if (m_pointLabelsClipping) | 
| 524 |             painter->setClipping(true); | 
| 525 |         else | 
| 526 |             painter->setClipping(false); | 
| 527 |         m_series->d_func()->drawSeriesPointLabels(painter, points: m_points, offset: pointLabelsOffset); | 
| 528 |     } | 
| 529 |  | 
| 530 |     painter->setPen(m_pointPen); | 
| 531 |     if (m_series->chart()->chartType() == QChart::ChartTypePolar && m_pointsVisible) { | 
| 532 |         painter->drawPoints(points: m_visiblePoints); | 
| 533 |     } else { | 
| 534 |         const bool simpleDraw = m_selectedPoints.isEmpty() && m_pointsConfiguration.isEmpty(); | 
| 535 |         painter->setPen(Qt::NoPen); | 
| 536 |         painter->setBrush(m_linePen.color()); | 
| 537 |         painter->setClipping(true); | 
| 538 |  | 
| 539 |         if (m_pointsVisible && simpleDraw && m_series->lightMarker().isNull()) { | 
| 540 |             for (int i = 0; i < m_points.size(); ++i) | 
| 541 |                 painter->drawEllipse(center: m_points.at(i), rx: m_markerSize, ry: m_markerSize); | 
| 542 |         } else if (!simpleDraw) { | 
| 543 |             qreal ptSize = m_markerSize; | 
| 544 |             for (int i = 0; i < m_points.size(); ++i) { | 
| 545 |                 if (clipRect.contains(p: m_points.at(i))) { | 
| 546 |                     painter->save(); | 
| 547 |                     ptSize = m_markerSize; | 
| 548 |                     bool drawPoint = m_pointsVisible && m_series->lightMarker().isNull(); | 
| 549 |                     if (m_pointsConfiguration.contains(key: i)) { | 
| 550 |                         const auto &conf = m_pointsConfiguration[i]; | 
| 551 |                         if (conf.contains(key: QXYSeries::PointConfiguration::Visibility)) { | 
| 552 |                             drawPoint = | 
| 553 |                                     m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility] | 
| 554 |                                     .toBool(); | 
| 555 |                         } | 
| 556 |  | 
| 557 |                         if (drawPoint) { | 
| 558 |                             if (conf.contains(key: QXYSeries::PointConfiguration::Size)) { | 
| 559 |                                 ptSize = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Size] | 
| 560 |                                         .toReal(); | 
| 561 |                             } | 
| 562 |  | 
| 563 |                             if (conf.contains(key: QXYSeries::PointConfiguration::Color)) { | 
| 564 |                                 painter->setBrush( | 
| 565 |                                             m_pointsConfiguration[i][QXYSeries::PointConfiguration::Color] | 
| 566 |                                         .value<QColor>()); | 
| 567 |                             } | 
| 568 |                         } | 
| 569 |                     } | 
| 570 |  | 
| 571 |                     if (m_series->isPointSelected(index: i)) { | 
| 572 |                         // Selected points are drawn regardless of m_pointsVisible settings and | 
| 573 |                         // custom point configuration. However, they are not drawn if light markers | 
| 574 |                         // are used. The reason of this is to avoid displaying selected point | 
| 575 |                         // over selected light marker. | 
| 576 |                         drawPoint = m_series->selectedLightMarker().isNull(); | 
| 577 |                         ptSize = ptSize * 1.5; | 
| 578 |                         if (m_selectedColor.isValid()) | 
| 579 |                             painter->setBrush(m_selectedColor); | 
| 580 |                     } | 
| 581 |  | 
| 582 |                     if (drawPoint) | 
| 583 |                         painter->drawEllipse(center: m_points.at(i), rx: ptSize, ry: ptSize); | 
| 584 |                     painter->restore(); | 
| 585 |                 } | 
| 586 |             } | 
| 587 |         } | 
| 588 |     } | 
| 589 |     painter->restore(); | 
| 590 | } | 
| 591 |  | 
| 592 | void SplineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event) | 
| 593 | { | 
| 594 |     QPointF matchedP = matchForLightMarker(eventPos: event->pos()); | 
| 595 |     if (!qIsNaN(d: matchedP.x())) | 
| 596 |         emit XYChart::pressed(point: matchedP); | 
| 597 |     else | 
| 598 |         emit XYChart::pressed(point: domain()->calculateDomainPoint(point: event->pos())); | 
| 599 |  | 
| 600 |     m_lastMousePos = event->pos(); | 
| 601 |     m_mousePressed = true; | 
| 602 |     QGraphicsItem::mousePressEvent(event); | 
| 603 | } | 
| 604 |  | 
| 605 | void SplineChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) | 
| 606 | { | 
| 607 |     // Identical code in LineChartItem | 
| 608 |     const QPointF matchedP = hoverPoint(eventPos: event->pos()); | 
| 609 |     m_lastHoveredMatchedPos = matchedP; | 
| 610 |     emit XYChart::hovered(point: matchedP, state: true); | 
| 611 |  | 
| 612 |     QGraphicsItem::hoverEnterEvent(event); | 
| 613 | } | 
| 614 |  | 
| 615 | void SplineChartItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) | 
| 616 | { | 
| 617 |     const QPointF matchedP = hoverPoint(eventPos: event->pos()); | 
| 618 |     if (!fuzzyComparePointF(p1: matchedP, p2: m_lastHoveredMatchedPos)) { | 
| 619 |         emit XYChart::hovered(point: matchedP, state: true); | 
| 620 |         m_lastHoveredMatchedPos = matchedP; | 
| 621 |     } | 
| 622 |  | 
| 623 |     QGraphicsItem::hoverMoveEvent(event); | 
| 624 | } | 
| 625 |  | 
| 626 | void SplineChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) | 
| 627 | { | 
| 628 |     const QPointF matchedP = hoverPoint(eventPos: event->pos()); | 
| 629 |     emit XYChart::hovered(point: matchedP, state: false); | 
| 630 |     m_lastHoveredMatchedPos = {qQNaN(), qQNaN()}; | 
| 631 |  | 
| 632 |     QGraphicsItem::hoverLeaveEvent(event); | 
| 633 | } | 
| 634 |  | 
| 635 | void SplineChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) | 
| 636 | { | 
| 637 |     QPointF result; | 
| 638 |     QPointF matchedP = matchForLightMarker(eventPos: m_lastMousePos); | 
| 639 |     if (!qIsNaN(d: matchedP.x())) | 
| 640 |         result = matchedP; | 
| 641 |     else | 
| 642 |         result = domain()->calculateDomainPoint(point: m_lastMousePos); | 
| 643 |  | 
| 644 |     emit XYChart::released(point: result); | 
| 645 |     if (m_mousePressed) | 
| 646 |         emit XYChart::clicked(point: result); | 
| 647 |     m_mousePressed = false; | 
| 648 |     QGraphicsItem::mouseReleaseEvent(event); | 
| 649 | } | 
| 650 |  | 
| 651 | void SplineChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) | 
| 652 | { | 
| 653 |     QPointF matchedP = matchForLightMarker(eventPos: event->pos()); | 
| 654 |     if (!qIsNaN(d: matchedP.x())) | 
| 655 |         emit XYChart::doubleClicked(point: matchedP); | 
| 656 |     else | 
| 657 |         emit XYChart::doubleClicked(point: domain()->calculateDomainPoint(point: m_lastMousePos)); | 
| 658 |  | 
| 659 |     QGraphicsItem::mouseDoubleClickEvent(event); | 
| 660 | } | 
| 661 |  | 
| 662 | QT_END_NAMESPACE | 
| 663 |  | 
| 664 | #include "moc_splinechartitem_p.cpp" | 
| 665 |  |