| 1 | // Copyright (C) 2021 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only | 
| 3 |  | 
| 4 | #include <private/xychart_p.h> | 
| 5 | #include <QtCharts/QXYSeries> | 
| 6 | #include <private/qxyseries_p.h> | 
| 7 | #include <private/chartpresenter_p.h> | 
| 8 | #include <private/abstractdomain_p.h> | 
| 9 | #include <private/chartdataset_p.h> | 
| 10 | #include <private/glxyseriesdata_p.h> | 
| 11 | #include <QtCharts/QXYModelMapper> | 
| 12 | #include <private/qabstractaxis_p.h> | 
| 13 | #include <QtGui/QPainter> | 
| 14 | #include <QtCore/QAbstractItemModel> | 
| 15 |  | 
| 16 |  | 
| 17 | QT_BEGIN_NAMESPACE | 
| 18 |  | 
| 19 | XYChart::XYChart(QXYSeries *series, QGraphicsItem *item): | 
| 20 |       ChartItem(series->d_func(),item), | 
| 21 |       m_series(series), | 
| 22 |       m_animation(0), | 
| 23 |       m_dirty(true) | 
| 24 | { | 
| 25 |     connect(sender: series->d_func(), signal: &QXYSeriesPrivate::seriesUpdated, | 
| 26 |             context: this, slot: &XYChart::handleSeriesUpdated); | 
| 27 |     connect(sender: series, signal: &QXYSeries::pointReplaced, context: this, slot: &XYChart::handlePointReplaced); | 
| 28 |     connect(sender: series, signal: &QXYSeries::pointsReplaced, context: this, slot: &XYChart::handlePointsReplaced); | 
| 29 |     connect(sender: series, signal: &QXYSeries::pointAdded, context: this, slot: &XYChart::handlePointAdded); | 
| 30 |     connect(sender: series, signal: &QXYSeries::pointRemoved, context: this, slot: &XYChart::handlePointRemoved); | 
| 31 |     connect(sender: series, signal: &QXYSeries::pointsRemoved, context: this, slot: &XYChart::handlePointsRemoved); | 
| 32 |     connect(sender: this, signal: &XYChart::clicked, context: series, slot: &QXYSeries::clicked); | 
| 33 |     connect(sender: this, signal: &XYChart::hovered, context: series, slot: &QXYSeries::hovered); | 
| 34 |     connect(sender: this, signal: &XYChart::pressed, context: series, slot: &QXYSeries::pressed); | 
| 35 |     connect(sender: this, signal: &XYChart::released, context: series, slot: &QXYSeries::released); | 
| 36 |     connect(sender: this, signal: &XYChart::doubleClicked, context: series, slot: &QXYSeries::doubleClicked); | 
| 37 |     connect(sender: series, signal: &QAbstractSeries::useOpenGLChanged, context: this, slot: &XYChart::handleDomainUpdated); | 
| 38 | } | 
| 39 |  | 
| 40 | void XYChart::setGeometryPoints(const QList<QPointF> &points) | 
| 41 | { | 
| 42 |     m_points = points; | 
| 43 | } | 
| 44 |  | 
| 45 | void XYChart::setAnimation(XYAnimation *animation) | 
| 46 | { | 
| 47 |     m_animation = animation; | 
| 48 | } | 
| 49 |  | 
| 50 | void XYChart::setDirty(bool dirty) | 
| 51 | { | 
| 52 |     m_dirty = dirty; | 
| 53 | } | 
| 54 |  | 
| 55 | // Returns a list with same size as geometryPoints list, indicating | 
| 56 | // the off grid status of points. | 
| 57 | QList<bool> XYChart::offGridStatusVector() | 
| 58 | { | 
| 59 |     qreal minX = domain()->minX(); | 
| 60 |     qreal maxX = domain()->maxX(); | 
| 61 |     qreal minY = domain()->minY(); | 
| 62 |     qreal maxY = domain()->maxY(); | 
| 63 |  | 
| 64 |     QList<bool> returnVector; | 
| 65 |     returnVector.resize(size: m_points.size()); | 
| 66 |     // During remove animation series may have different number of points, | 
| 67 |     // so ensure we don't go over the index. No need to check for zero points, this | 
| 68 |     // will not be called in such a situation. | 
| 69 |     const int seriesLastIndex = m_series->count() - 1; | 
| 70 |  | 
| 71 |     for (int i = 0; i < m_points.size(); i++) { | 
| 72 |         const QPointF &seriesPoint = m_series->at(index: qMin(a: seriesLastIndex, b: i)); | 
| 73 |         if (seriesPoint.x() < minX | 
| 74 |             || seriesPoint.x() > maxX | 
| 75 |             || seriesPoint.y() < minY | 
| 76 |             || seriesPoint.y() > maxY) { | 
| 77 |             returnVector[i] = true; | 
| 78 |         } else { | 
| 79 |             returnVector[i] = false; | 
| 80 |         } | 
| 81 |     } | 
| 82 |     return returnVector; | 
| 83 | } | 
| 84 |  | 
| 85 | void XYChart::updateChart(const QList<QPointF> &oldPoints, const QList<QPointF> &newPoints, | 
| 86 |                           int index) | 
| 87 | { | 
| 88 |  | 
| 89 |     if (m_animation) { | 
| 90 |         m_animation->setup(oldPoints, newPoints, index); | 
| 91 |         m_points = newPoints; | 
| 92 |         setDirty(false); | 
| 93 |         presenter()->startAnimation(animation: m_animation); | 
| 94 |     } else { | 
| 95 |         m_points = newPoints; | 
| 96 |         updateGeometry(); | 
| 97 |     } | 
| 98 | } | 
| 99 |  | 
| 100 | void XYChart::updateGlChart() | 
| 101 | { | 
| 102 |     dataSet()->glXYSeriesDataManager()->setPoints(series: m_series, domain: domain()); | 
| 103 |     presenter()->updateGLWidget(); | 
| 104 |     updateGeometry(); | 
| 105 | } | 
| 106 |  | 
| 107 | // Doesn't update gl geometry, but refreshes the chart | 
| 108 | void XYChart::refreshGlChart() | 
| 109 | { | 
| 110 |     if (presenter()) | 
| 111 |         presenter()->updateGLWidget(); | 
| 112 | } | 
| 113 |  | 
| 114 | //handlers | 
| 115 |  | 
| 116 | void XYChart::handlePointAdded(int index) | 
| 117 | { | 
| 118 |     Q_ASSERT(index < m_series->count()); | 
| 119 |     Q_ASSERT(index >= 0); | 
| 120 |  | 
| 121 |     if (m_series->useOpenGL()) { | 
| 122 |         updateGlChart(); | 
| 123 |     } else { | 
| 124 |         QList<QPointF> points; | 
| 125 |         if (m_dirty || m_points.isEmpty()) { | 
| 126 |             points = domain()->calculateGeometryPoints(list: m_series->points()); | 
| 127 |         } else { | 
| 128 |             points = m_points; | 
| 129 |             QPointF point = | 
| 130 |                     domain()->calculateGeometryPoint(point: m_series->points().at(i: index), ok&: m_validData); | 
| 131 |             if (!m_validData) | 
| 132 |                 m_points.clear(); | 
| 133 |             else | 
| 134 |                 points.insert(i: index, t: point); | 
| 135 |         } | 
| 136 |         updateChart(oldPoints: m_points, newPoints: points, index); | 
| 137 |     } | 
| 138 | } | 
| 139 |  | 
| 140 | void XYChart::handlePointRemoved(int index) | 
| 141 | { | 
| 142 |     Q_ASSERT(index <= m_series->count()); | 
| 143 |     Q_ASSERT(index >= 0); | 
| 144 |  | 
| 145 |     if (m_series->useOpenGL()) { | 
| 146 |         updateGlChart(); | 
| 147 |     } else { | 
| 148 |         QList<QPointF> points; | 
| 149 |         if (m_dirty || m_points.isEmpty()) { | 
| 150 |             points = domain()->calculateGeometryPoints(list: m_series->points()); | 
| 151 |         } else { | 
| 152 |             points = m_points; | 
| 153 |             points.remove(i: index); | 
| 154 |         } | 
| 155 |         updateChart(oldPoints: m_points, newPoints: points, index); | 
| 156 |     } | 
| 157 | } | 
| 158 |  | 
| 159 | void XYChart::handlePointsRemoved(int index, int count) | 
| 160 | { | 
| 161 |     Q_ASSERT(index <= m_series->count()); | 
| 162 |     Q_ASSERT(index >= 0); | 
| 163 |  | 
| 164 |     if (m_series->useOpenGL()) { | 
| 165 |         updateGlChart(); | 
| 166 |     } else { | 
| 167 |         QList<QPointF> points; | 
| 168 |         if (m_dirty || m_points.isEmpty()) { | 
| 169 |             points = domain()->calculateGeometryPoints(list: m_series->points()); | 
| 170 |         } else { | 
| 171 |             points = m_points; | 
| 172 |             points.remove(i: index, n: count); | 
| 173 |         } | 
| 174 |         updateChart(oldPoints: m_points, newPoints: points, index); | 
| 175 |     } | 
| 176 | } | 
| 177 |  | 
| 178 | void XYChart::handlePointReplaced(int index) | 
| 179 | { | 
| 180 |     Q_ASSERT(index < m_series->count()); | 
| 181 |     Q_ASSERT(index >= 0); | 
| 182 |  | 
| 183 |     if (m_series->useOpenGL()) { | 
| 184 |         updateGlChart(); | 
| 185 |     } else { | 
| 186 |         QList<QPointF> points; | 
| 187 |         if (m_dirty || m_points.isEmpty()) { | 
| 188 |             points = domain()->calculateGeometryPoints(list: m_series->points()); | 
| 189 |         } else { | 
| 190 |             QPointF point = | 
| 191 |                     domain()->calculateGeometryPoint(point: m_series->points().at(i: index), ok&: m_validData); | 
| 192 |             if (!m_validData) | 
| 193 |                 m_points.clear(); | 
| 194 |             points = m_points; | 
| 195 |             if (m_validData) | 
| 196 |                 points.replace(i: index, t: point); | 
| 197 |         } | 
| 198 |         updateChart(oldPoints: m_points, newPoints: points, index); | 
| 199 |     } | 
| 200 | } | 
| 201 |  | 
| 202 | void XYChart::handlePointsReplaced() | 
| 203 | { | 
| 204 |     if (m_series->useOpenGL()) { | 
| 205 |         updateGlChart(); | 
| 206 |     } else { | 
| 207 |         // All the points were replaced -> recalculate | 
| 208 |         QList<QPointF> points = domain()->calculateGeometryPoints(list: m_series->points()); | 
| 209 |         updateChart(oldPoints: m_points, newPoints: points, index: -1); | 
| 210 |     } | 
| 211 | } | 
| 212 |  | 
| 213 | void XYChart::handleDomainUpdated() | 
| 214 | { | 
| 215 |     if (m_series->useOpenGL()) { | 
| 216 |         updateGlChart(); | 
| 217 |     } else { | 
| 218 |         if (isEmpty()) return; | 
| 219 |         QList<QPointF> points = domain()->calculateGeometryPoints(list: m_series->points()); | 
| 220 |         updateChart(oldPoints: m_points, newPoints: points); | 
| 221 |     } | 
| 222 | } | 
| 223 |  | 
| 224 | void XYChart::handleSeriesUpdated() | 
| 225 | { | 
| 226 | } | 
| 227 |  | 
| 228 | bool XYChart::isEmpty() | 
| 229 | { | 
| 230 |     return domain()->isEmpty() || m_series->points().isEmpty(); | 
| 231 | } | 
| 232 |  | 
| 233 | QPointF XYChart::matchForLightMarker(const QPointF &eventPos) const | 
| 234 | { | 
| 235 |     if (m_series->lightMarker().isNull() | 
| 236 |             && (m_series->selectedLightMarker().isNull() | 
| 237 |                 || m_series->selectedPoints().isEmpty())) | 
| 238 |         return QPointF(qQNaN(), qQNaN()); // 0,0 could actually be in points() | 
| 239 |  | 
| 240 |     const bool useSelectedMarker = m_series->lightMarker().isNull(); | 
| 241 |  | 
| 242 |     QList<QPointF> points; | 
| 243 |     if (useSelectedMarker) { | 
| 244 |         const auto selectedPoints = m_series->selectedPoints(); | 
| 245 |         for (const int &selectedPointIndex : selectedPoints) | 
| 246 |             points << m_series->at(index: selectedPointIndex); | 
| 247 |     } else { | 
| 248 |         points = m_series->points(); | 
| 249 |     } | 
| 250 |  | 
| 251 |     for (const QPointF &dp : points) { | 
| 252 |         bool ok; | 
| 253 |         const QPointF gp = domain()->calculateGeometryPoint(point: dp, ok); | 
| 254 |         if (ok) { | 
| 255 |             // '+2' and '+4': There is an addRect for the (mouse-)shape | 
| 256 |             // in LineChartItem::updateGeometry() | 
| 257 |             // This has a margin of 1 to make sure a press in the icon will always be detected, | 
| 258 |             // but as there is a bunch of 'translations' and therefore inaccuracies, | 
| 259 |             // so it is necessary to increase that margin to 2 | 
| 260 |             // (otherwise you can click next to an icon, get a click event but not match it) | 
| 261 |             QRectF r(gp.x() - (m_series->markerSize() / 2 + 2), | 
| 262 |                      gp.y() - (m_series->markerSize() / 2 + 2), | 
| 263 |                      m_series->markerSize() + 4, m_series->markerSize() + 4); | 
| 264 |  | 
| 265 |             if (r.contains(p: eventPos)) | 
| 266 |                 return dp; | 
| 267 |         } | 
| 268 |     } | 
| 269 |     return QPointF(qQNaN(), qQNaN()); // 0,0 could actually be in points() | 
| 270 | } | 
| 271 |  | 
| 272 | QPointF XYChart::hoverPoint(const QPointF &eventPos) const | 
| 273 | { | 
| 274 |     const QPointF result = matchForLightMarker(eventPos); | 
| 275 |     return qIsNaN(d: result.x()) | 
| 276 |         ? domain()->calculateDomainPoint(point: eventPos) | 
| 277 |         : result; | 
| 278 | } | 
| 279 |  | 
| 280 | bool XYChart::fuzzyComparePointF(const QPointF &p1, const QPointF &p2) | 
| 281 | { | 
| 282 |     // Should normally not be NaN, fail on safe side | 
| 283 |     return !qIsNaN(d: p1.x()) && !qIsNaN(d: p2.x()) | 
| 284 |            && qFuzzyCompare(p1: p1.x(), p2: p2.x()) && qFuzzyCompare(p1: p1.y(), p2: p2.y()); | 
| 285 | } | 
| 286 |  | 
| 287 | QT_END_NAMESPACE | 
| 288 |  | 
| 289 | #include "moc_xychart_p.cpp" | 
| 290 |  |