1 | // Copyright (C) 2023 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include <QtGraphs/qlineseries.h> |
5 | #include <QtGraphs/qscatterseries.h> |
6 | #include <QtGraphs/qsplineseries.h> |
7 | #include <private/pointrenderer_p.h> |
8 | #include <private/axisrenderer_p.h> |
9 | #include <private/qabstractseries_p.h> |
10 | #include <private/qgraphsview_p.h> |
11 | #include <private/qxyseries_p.h> |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | static const char *TAG_POINT_COLOR = "pointColor"; |
16 | static const char *TAG_POINT_BORDER_COLOR = "pointBorderColor"; |
17 | static const char *TAG_POINT_BORDER_WIDTH = "pointBorderWidth"; |
18 | static const char *TAG_POINT_SELECTED_COLOR = "pointSelectedColor"; |
19 | static const char *TAG_POINT_SELECTED = "pointSelected"; |
20 | static const char *TAG_POINT_VALUE_X = "pointValueX"; |
21 | static const char *TAG_POINT_VALUE_Y = "pointValueY"; |
22 | |
23 | PointRenderer::PointRenderer(QGraphsView *graph) |
24 | : QQuickItem(graph) |
25 | , m_graph(graph) |
26 | { |
27 | setFlag(flag: QQuickItem::ItemHasContents); |
28 | setClip(true); |
29 | m_shape.setParentItem(this); |
30 | m_shape.setPreferredRendererType(QQuickShape::CurveRenderer); |
31 | |
32 | const QString qmlData = QLatin1StringView(R"QML( |
33 | import QtQuick; |
34 | |
35 | Rectangle { |
36 | property bool pointSelected |
37 | property color pointColor |
38 | property color pointBorderColor |
39 | property color pointSelectedColor |
40 | property real pointBorderWidth |
41 | color: pointSelected ? pointSelectedColor : pointColor |
42 | border.color: pointBorderColor |
43 | border.width: pointBorderWidth |
44 | width: %1 |
45 | height: %1 |
46 | } |
47 | )QML").arg(args: QString::number((int) defaultSize())); |
48 | m_tempMarker = new QQmlComponent(qmlEngine(m_graph), this); |
49 | m_tempMarker->setData(qmlData.toUtf8(), baseUrl: QUrl()); |
50 | } |
51 | |
52 | PointRenderer::~PointRenderer() |
53 | { |
54 | qDeleteAll(c: m_groups); |
55 | } |
56 | |
57 | qreal PointRenderer::defaultSize(QXYSeries *series) |
58 | { |
59 | qreal size = 16.0; |
60 | if (series != nullptr) { |
61 | if (auto line = qobject_cast<QLineSeries *>(object: series)) |
62 | size = qMax(a: size, b: line->width()); |
63 | else if (auto spline = qobject_cast<QSplineSeries *>(object: series)) |
64 | size = qMax(a: size, b: spline->width()); |
65 | } |
66 | return size; |
67 | } |
68 | |
69 | void PointRenderer::calculateRenderCoordinates( |
70 | AxisRenderer *axisRenderer, qreal origX, qreal origY, qreal *renderX, qreal *renderY) |
71 | { |
72 | auto flipX = axisRenderer->m_axisHorizontalMaxValue < axisRenderer->m_axisHorizontalMinValue |
73 | ? -1 |
74 | : 1; |
75 | auto flipY = axisRenderer->m_axisVerticalMaxValue < axisRenderer->m_axisVerticalMinValue ? -1 |
76 | : 1; |
77 | |
78 | *renderX = m_areaWidth * flipX * origX * m_maxHorizontal - m_horizontalOffset; |
79 | *renderY = m_areaHeight - m_areaHeight * flipY * origY * m_maxVertical |
80 | + m_verticalOffset; |
81 | } |
82 | |
83 | void PointRenderer::reverseRenderCoordinates( |
84 | AxisRenderer *axisRenderer, qreal renderX, qreal renderY, qreal *origX, qreal *origY) |
85 | { |
86 | auto flipX = axisRenderer->m_axisHorizontalMaxValue < axisRenderer->m_axisHorizontalMinValue |
87 | ? -1 |
88 | : 1; |
89 | auto flipY = axisRenderer->m_axisVerticalMaxValue < axisRenderer->m_axisVerticalMinValue ? -1 |
90 | : 1; |
91 | |
92 | *origX = (renderX + m_horizontalOffset) / (m_areaWidth * flipX * m_maxHorizontal); |
93 | *origY = (renderY - m_areaHeight - m_verticalOffset) |
94 | / (-1 * m_areaHeight * flipY * m_maxVertical); |
95 | } |
96 | |
97 | void PointRenderer::updatePointDelegate( |
98 | QXYSeries *series, PointGroup *group, qsizetype pointIndex, qreal x, qreal y) |
99 | { |
100 | auto theme = m_graph->theme(); |
101 | auto marker = group->markers[pointIndex]; |
102 | auto &rect = group->rects[pointIndex]; |
103 | |
104 | const auto &seriesColors = theme->seriesColors(); |
105 | const auto &borderColors = theme->borderColors(); |
106 | qsizetype index = group->colorIndex % seriesColors.size(); |
107 | QColor color = series->color().alpha() != 0 ? series->color() : seriesColors.at(i: index); |
108 | index = group->colorIndex % borderColors.size(); |
109 | QColor borderColor = borderColors.at(i: index); |
110 | qreal borderWidth = theme->borderWidth(); |
111 | QColor selectedColor = series->selectedColor().alpha() != 0 |
112 | ? series->selectedColor() |
113 | : m_graph->theme()->singleHighlightColor(); |
114 | if (marker->property(name: TAG_POINT_SELECTED).isValid()) |
115 | marker->setProperty(name: TAG_POINT_SELECTED, value: series->isPointSelected(index: pointIndex)); |
116 | if (marker->property(name: TAG_POINT_COLOR).isValid()) |
117 | marker->setProperty(name: TAG_POINT_COLOR, value: color); |
118 | if (marker->property(name: TAG_POINT_BORDER_COLOR).isValid()) |
119 | marker->setProperty(name: TAG_POINT_BORDER_COLOR, value: borderColor); |
120 | if (marker->property(name: TAG_POINT_BORDER_WIDTH).isValid()) |
121 | marker->setProperty(name: TAG_POINT_BORDER_WIDTH, value: borderWidth); |
122 | if (marker->property(name: TAG_POINT_SELECTED_COLOR).isValid()) |
123 | marker->setProperty(name: TAG_POINT_SELECTED_COLOR, value: selectedColor); |
124 | const auto point = series->points().at(i: pointIndex); |
125 | if (marker->property(name: TAG_POINT_VALUE_X).isValid()) |
126 | marker->setProperty(name: TAG_POINT_VALUE_X, value: point.x()); |
127 | if (marker->property(name: TAG_POINT_VALUE_Y).isValid()) |
128 | marker->setProperty(name: TAG_POINT_VALUE_Y, value: point.y()); |
129 | |
130 | marker->setX(x - marker->width() / 2.0); |
131 | marker->setY(y - marker->height() / 2.0); |
132 | marker->setVisible(true); |
133 | |
134 | rect = QRectF(x - marker->width() / 2.0, |
135 | y - marker->height() / 2.0, |
136 | marker->width(), |
137 | marker->height()); |
138 | } |
139 | |
140 | void PointRenderer::hidePointDelegates(QXYSeries *series) |
141 | { |
142 | auto *group = m_groups.value(key: series); |
143 | if (group->currentMarker) { |
144 | for (int i = 0; i < group->markers.size(); ++i) { |
145 | auto *marker = group->markers[i]; |
146 | marker->setVisible(false); |
147 | } |
148 | } |
149 | group->rects.clear(); |
150 | } |
151 | |
152 | void PointRenderer::updateLegendData(QXYSeries *series, QLegendData &legendData) |
153 | { |
154 | QList<QLegendData> legendDataList = {legendData}; |
155 | series->d_func()->setLegendData(legendDataList); |
156 | } |
157 | |
158 | void PointRenderer::updateScatterSeries(QScatterSeries *series, QLegendData &legendData) |
159 | { |
160 | if (series->isVisible()) { |
161 | auto group = m_groups.value(key: series); |
162 | auto &&points = series->points(); |
163 | group->rects.resize(size: points.size()); |
164 | for (int i = 0; i < points.size(); ++i) { |
165 | qreal x, y; |
166 | calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer, origX: points[i].x(), origY: points[i].y(), renderX: &x, renderY: &y); |
167 | if (group->currentMarker) { |
168 | updatePointDelegate(series, group, pointIndex: i, x, y); |
169 | } else { |
170 | auto &rect = group->rects[i]; |
171 | qreal size = defaultSize(series); |
172 | rect = QRectF(x - size / 2.0, y - size / 2.0, size, size); |
173 | } |
174 | } |
175 | } else { |
176 | hidePointDelegates(series); |
177 | } |
178 | // TODO: When fill color is added to the scatterseries use it instead for |
179 | // the color. QTBUG-122434 |
180 | legendData = {.color: series->color(), .borderColor: series->color(), .label: series->name()}; |
181 | } |
182 | |
183 | void PointRenderer::updateLineSeries(QLineSeries *series, QLegendData &legendData) |
184 | { |
185 | auto theme = m_graph->theme(); |
186 | auto group = m_groups.value(key: series); |
187 | |
188 | const auto &seriesColors = theme->seriesColors(); |
189 | qsizetype index = group->colorIndex % seriesColors.size(); |
190 | QColor color = series->color().alpha() != 0 ? series->color() : seriesColors.at(i: index); |
191 | |
192 | group->shapePath->setStrokeColor(color); |
193 | group->shapePath->setStrokeWidth(series->width()); |
194 | group->shapePath->setFillColor(QColorConstants::Transparent); |
195 | |
196 | Qt::PenCapStyle capStyle = series->capStyle(); |
197 | if (capStyle == Qt::PenCapStyle::SquareCap) |
198 | group->shapePath->setCapStyle(QQuickShapePath::CapStyle::SquareCap); |
199 | else if (capStyle == Qt::PenCapStyle::FlatCap) |
200 | group->shapePath->setCapStyle(QQuickShapePath::CapStyle::FlatCap); |
201 | else if (capStyle == Qt::PenCapStyle::RoundCap) |
202 | group->shapePath->setCapStyle(QQuickShapePath::CapStyle::RoundCap); |
203 | |
204 | auto &painterPath = group->painterPath; |
205 | painterPath.clear(); |
206 | |
207 | if (series->isVisible()) { |
208 | auto &&points = series->points(); |
209 | group->rects.resize(size: points.size()); |
210 | for (int i = 0; i < points.size(); ++i) { |
211 | qreal x, y; |
212 | calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer, origX: points[i].x(), origY: points[i].y(), renderX: &x, renderY: &y); |
213 | if (i == 0) |
214 | painterPath.moveTo(x, y); |
215 | else |
216 | painterPath.lineTo(x, y); |
217 | |
218 | if (group->currentMarker) { |
219 | updatePointDelegate(series, group, pointIndex: i, x, y); |
220 | } else { |
221 | auto &rect = group->rects[i]; |
222 | qreal size = defaultSize(series); |
223 | rect = QRectF(x - size / 2.0, y - size / 2.0, size, size); |
224 | } |
225 | } |
226 | } else { |
227 | hidePointDelegates(series); |
228 | } |
229 | group->shapePath->setPath(painterPath); |
230 | legendData = {.color: color, .borderColor: color, .label: series->name()}; |
231 | } |
232 | |
233 | void PointRenderer::updateSplineSeries(QSplineSeries *series, QLegendData &legendData) |
234 | { |
235 | auto theme = m_graph->theme(); |
236 | auto group = m_groups.value(key: series); |
237 | |
238 | const auto &seriesColors = theme->seriesColors(); |
239 | qsizetype index = group->colorIndex % seriesColors.size(); |
240 | QColor color = series->color().alpha() != 0 ? series->color() : seriesColors.at(i: index); |
241 | |
242 | group->shapePath->setStrokeColor(color); |
243 | group->shapePath->setStrokeWidth(series->width()); |
244 | group->shapePath->setFillColor(QColorConstants::Transparent); |
245 | |
246 | Qt::PenCapStyle capStyle = series->capStyle(); |
247 | if (capStyle == Qt::PenCapStyle::SquareCap) |
248 | group->shapePath->setCapStyle(QQuickShapePath::CapStyle::SquareCap); |
249 | else if (capStyle == Qt::PenCapStyle::FlatCap) |
250 | group->shapePath->setCapStyle(QQuickShapePath::CapStyle::FlatCap); |
251 | else if (capStyle == Qt::PenCapStyle::RoundCap) |
252 | group->shapePath->setCapStyle(QQuickShapePath::CapStyle::RoundCap); |
253 | |
254 | auto &painterPath = group->painterPath; |
255 | painterPath.clear(); |
256 | |
257 | if (series->isVisible()) { |
258 | auto &&points = series->points(); |
259 | group->rects.resize(size: points.size()); |
260 | auto fittedPoints = series->getControlPoints(); |
261 | |
262 | for (int i = 0, j = 0; i < points.size(); ++i, ++j) { |
263 | qreal x, y; |
264 | calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer, origX: points[i].x(), origY: points[i].y(), renderX: &x, renderY: &y); |
265 | |
266 | if (i == 0) { |
267 | painterPath.moveTo(x, y); |
268 | } else { |
269 | qreal x1, y1, x2, y2; |
270 | calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer, |
271 | origX: fittedPoints[j - 1].x(), |
272 | origY: fittedPoints[j - 1].y(), |
273 | renderX: &x1, |
274 | renderY: &y1); |
275 | calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer, |
276 | origX: fittedPoints[j].x(), |
277 | origY: fittedPoints[j].y(), |
278 | renderX: &x2, |
279 | renderY: &y2); |
280 | painterPath.cubicTo(ctrlPt1x: x1, ctrlPt1y: y1, ctrlPt2x: x2, ctrlPt2y: y2, endPtx: x, endPty: y); |
281 | ++j; |
282 | } |
283 | |
284 | if (group->currentMarker) { |
285 | updatePointDelegate(series, group, pointIndex: i, x, y); |
286 | } else { |
287 | auto &rect = group->rects[i]; |
288 | qreal size = defaultSize(series); |
289 | rect = QRectF(x - size / 2.0, y - size / 2.0, size, size); |
290 | } |
291 | } |
292 | } else { |
293 | hidePointDelegates(series); |
294 | } |
295 | |
296 | group->shapePath->setPath(painterPath); |
297 | legendData = {.color: color, .borderColor: color, .label: series->name()}; |
298 | } |
299 | |
300 | void PointRenderer::handlePolish(QXYSeries *series) |
301 | { |
302 | auto theme = m_graph->theme(); |
303 | if (!theme) |
304 | return; |
305 | |
306 | if (!m_graph->m_axisRenderer) |
307 | return; |
308 | |
309 | if (series->points().isEmpty()) { |
310 | auto group = m_groups.value(key: series); |
311 | |
312 | if (group) { |
313 | if (group->shapePath) { |
314 | auto &painterPath = group->painterPath; |
315 | painterPath.clear(); |
316 | group->shapePath->setPath(painterPath); |
317 | } |
318 | |
319 | for (auto m : group->markers) |
320 | m->deleteLater(); |
321 | |
322 | group->markers.clear(); |
323 | } |
324 | |
325 | return; |
326 | } |
327 | |
328 | if (width() <= 0 || height() <= 0) |
329 | return; |
330 | |
331 | m_areaWidth = width(); |
332 | m_areaHeight = height(); |
333 | |
334 | m_maxVertical = m_graph->m_axisRenderer->m_axisVerticalValueRange > 0 |
335 | ? 1.0 / m_graph->m_axisRenderer->m_axisVerticalValueRange |
336 | : 100.0; |
337 | m_maxHorizontal = m_graph->m_axisRenderer->m_axisHorizontalValueRange > 0 |
338 | ? 1.0 / m_graph->m_axisRenderer->m_axisHorizontalValueRange |
339 | : 100.0; |
340 | |
341 | auto vmin = m_graph->m_axisRenderer->m_axisVerticalMinValue |
342 | > m_graph->m_axisRenderer->m_axisVerticalMaxValue |
343 | ? std::abs(x: m_graph->m_axisRenderer->m_axisVerticalMinValue) |
344 | : m_graph->m_axisRenderer->m_axisVerticalMinValue; |
345 | |
346 | m_verticalOffset = (vmin / m_graph->m_axisRenderer->m_axisVerticalValueRange) * m_areaHeight; |
347 | |
348 | auto hmin = m_graph->m_axisRenderer->m_axisHorizontalMinValue |
349 | > m_graph->m_axisRenderer->m_axisHorizontalMaxValue |
350 | ? std::abs(x: m_graph->m_axisRenderer->m_axisHorizontalMinValue) |
351 | : m_graph->m_axisRenderer->m_axisHorizontalMinValue; |
352 | |
353 | m_horizontalOffset = (hmin / m_graph->m_axisRenderer->m_axisHorizontalValueRange) * m_areaWidth; |
354 | |
355 | if (!m_groups.contains(key: series)) { |
356 | PointGroup *group = new PointGroup(); |
357 | group->series = series; |
358 | m_groups.insert(key: series, value: group); |
359 | |
360 | if (series->type() != QAbstractSeries::SeriesType::Scatter) { |
361 | group->shapePath = new QQuickShapePath(&m_shape); |
362 | group->shapePath->setAsynchronous(true); |
363 | auto data = m_shape.data(); |
364 | data.append(&data, m_groups.value(key: series)->shapePath); |
365 | } |
366 | } |
367 | |
368 | auto group = m_groups.value(key: series); |
369 | |
370 | qsizetype pointCount = series->points().size(); |
371 | |
372 | if ((series->type() == QAbstractSeries::SeriesType::Scatter) && !series->pointDelegate()) |
373 | group->currentMarker = m_tempMarker; |
374 | else if (series->pointDelegate()) |
375 | group->currentMarker = series->pointDelegate(); |
376 | |
377 | if (group->currentMarker != group->previousMarker) { |
378 | for (auto &&marker : group->markers) |
379 | marker->deleteLater(); |
380 | group->markers.clear(); |
381 | } |
382 | group->previousMarker = group->currentMarker; |
383 | |
384 | if (group->currentMarker) { |
385 | qsizetype markerCount = group->markers.size(); |
386 | if (markerCount < pointCount) { |
387 | for (qsizetype i = markerCount; i < pointCount; ++i) { |
388 | QQuickItem *item = qobject_cast<QQuickItem *>( |
389 | o: group->currentMarker->create(context: group->currentMarker->creationContext())); |
390 | item->setParent(this); |
391 | item->setParentItem(this); |
392 | group->markers << item; |
393 | } |
394 | } else if (markerCount > pointCount) { |
395 | for (qsizetype i = pointCount; i < markerCount; ++i) |
396 | group->markers[i]->deleteLater(); |
397 | group->markers.resize(size: pointCount); |
398 | } |
399 | } else if (group->markers.size() > 0) { |
400 | for (auto &&marker : group->markers) |
401 | marker->deleteLater(); |
402 | group->markers.clear(); |
403 | } |
404 | |
405 | if (group->colorIndex < 0) { |
406 | group->colorIndex = m_graph->graphSeriesCount(); |
407 | m_graph->setGraphSeriesCount(group->colorIndex + 1); |
408 | } |
409 | |
410 | QLegendData legendData; |
411 | if (auto scatter = qobject_cast<QScatterSeries *>(object: series)) |
412 | updateScatterSeries(series: scatter, legendData); |
413 | else if (auto line = qobject_cast<QLineSeries *>(object: series)) |
414 | updateLineSeries(series: line, legendData); |
415 | else if (auto spline = qobject_cast<QSplineSeries *>(object: series)) |
416 | updateSplineSeries(series: spline, legendData); |
417 | |
418 | updateLegendData(series, legendData); |
419 | } |
420 | |
421 | void PointRenderer::afterPolish(QList<QAbstractSeries *> &cleanupSeries) |
422 | { |
423 | for (auto series : cleanupSeries) { |
424 | auto xySeries = qobject_cast<QXYSeries *>(object: series); |
425 | if (xySeries && m_groups.contains(key: xySeries)) { |
426 | auto group = m_groups.value(key: xySeries); |
427 | |
428 | for (auto marker : group->markers) |
429 | marker->deleteLater(); |
430 | |
431 | if (group->shapePath) { |
432 | auto painterPath = group->painterPath; |
433 | painterPath.clear(); |
434 | group->shapePath->setPath(painterPath); |
435 | } |
436 | |
437 | delete group; |
438 | m_groups.remove(key: xySeries); |
439 | } |
440 | } |
441 | } |
442 | |
443 | void PointRenderer::updateSeries(QXYSeries *series) |
444 | { |
445 | Q_UNUSED(series); |
446 | } |
447 | |
448 | void PointRenderer::afterUpdate(QList<QAbstractSeries *> &cleanupSeries) |
449 | { |
450 | Q_UNUSED(cleanupSeries); |
451 | } |
452 | |
453 | bool PointRenderer::handleMouseMove(QMouseEvent *event) |
454 | { |
455 | if (!m_pressedGroup || !m_pressedGroup->series->isVisible()) |
456 | return false; |
457 | |
458 | if (m_pointPressed && m_pressedGroup->series->isDraggable()) { |
459 | float w = width(); |
460 | float h = height(); |
461 | double maxVertical = m_graph->m_axisRenderer->m_axisVerticalValueRange > 0 |
462 | ? 1.0 / m_graph->m_axisRenderer->m_axisVerticalValueRange |
463 | : 100.0; |
464 | double maxHorizontal = m_graph->m_axisRenderer->m_axisHorizontalValueRange > 0 |
465 | ? 1.0 / m_graph->m_axisRenderer->m_axisHorizontalValueRange |
466 | : 100.0; |
467 | |
468 | QPoint delta = m_pressStart - event->pos(); |
469 | |
470 | qreal deltaX = -delta.x() / w / maxHorizontal; |
471 | qreal deltaY = delta.y() / h / maxVertical; |
472 | |
473 | QPointF point = m_pressedGroup->series->at(index: m_pressedPointIndex) + QPointF(deltaX, deltaY); |
474 | m_pressedGroup->series->replace(index: m_pressedPointIndex, newPoint: point); |
475 | |
476 | m_pressStart = event->pos(); |
477 | m_pointDragging = true; |
478 | |
479 | return true; |
480 | } |
481 | return false; |
482 | } |
483 | |
484 | bool PointRenderer::handleMousePress(QMouseEvent *event) |
485 | { |
486 | bool handled = false; |
487 | for (auto &&group : m_groups) { |
488 | if (!group->series->isVisible()) |
489 | continue; |
490 | |
491 | if (!group->series->isSelectable() && !group->series->isDraggable()) |
492 | continue; |
493 | |
494 | int index = 0; |
495 | for (auto &&rect : group->rects) { |
496 | if (rect.contains(p: event->pos())) { |
497 | m_pointPressed = true; |
498 | m_pressStart = event->pos(); |
499 | m_pressedGroup = group; |
500 | m_pressedPointIndex = index; |
501 | handled = true; |
502 | } |
503 | index++; |
504 | } |
505 | } |
506 | return handled; |
507 | } |
508 | |
509 | bool PointRenderer::handleMouseRelease(QMouseEvent *event) |
510 | { |
511 | bool handled = false; |
512 | if (!m_pointDragging && m_pointPressed && m_pressedGroup |
513 | && m_pressedGroup->series->isSelectable() && m_pressedGroup->series->isVisible()) { |
514 | if (m_pressedGroup->rects[m_pressedPointIndex].contains(p: event->pos())) { |
515 | if (m_pressedGroup->series->isPointSelected(index: m_pressedPointIndex)) { |
516 | m_pressedGroup->series->deselectPoint(index: m_pressedPointIndex); |
517 | } else { |
518 | m_pressedGroup->series->selectPoint(index: m_pressedPointIndex); |
519 | } |
520 | handled = true; |
521 | } |
522 | } |
523 | m_pointPressed = false; |
524 | m_pointDragging = false; |
525 | return handled; |
526 | } |
527 | |
528 | bool PointRenderer::handleHoverMove(QHoverEvent *event) |
529 | { |
530 | bool handled = false; |
531 | const QPointF &position = event->position(); |
532 | |
533 | for (auto &&group : m_groups) { |
534 | if (!group->series->isHoverable() || !group->series->isVisible()) |
535 | continue; |
536 | |
537 | auto axisRenderer = group->series->graph()->m_axisRenderer; |
538 | bool isHNegative = axisRenderer->m_axisHorizontalMaxValue |
539 | < axisRenderer->m_axisHorizontalMinValue; |
540 | bool isVNegative = axisRenderer->m_axisVerticalMaxValue |
541 | < axisRenderer->m_axisVerticalMinValue; |
542 | |
543 | if (group->series->type() == QAbstractSeries::SeriesType::Scatter) { |
544 | const QString &name = group->series->name(); |
545 | |
546 | bool hovering = false; |
547 | |
548 | int index = 0; |
549 | for (auto &&rect : group->rects) { |
550 | if (rect.contains(p: position.toPoint())) { |
551 | if (!group->hover) { |
552 | group->hover = true; |
553 | emit group->series->hoverEnter(seriesName: name, position, value: group->series->at(index)); |
554 | } |
555 | emit group->series->hover(seriesName: name, position, value: group->series->at(index)); |
556 | hovering = true; |
557 | } |
558 | index++; |
559 | } |
560 | |
561 | if (!hovering && group->hover) { |
562 | group->hover = false; |
563 | emit group->series->hoverExit(seriesName: name, position); |
564 | } |
565 | } else { |
566 | const qreal x0 = event->position().x(); |
567 | const qreal y0 = event->position().y(); |
568 | |
569 | const qreal hoverSize = defaultSize(series: group->series) / 2.0; |
570 | const QString &name = group->series->name(); |
571 | auto &&points = group->series->points(); |
572 | // True when line, false when spline |
573 | const bool isLine = group->series->type() == QAbstractSeries::SeriesType::Line; |
574 | if (points.size() >= 2) { |
575 | bool hovering = false; |
576 | auto subpath = group->painterPath.toSubpathPolygons(); |
577 | |
578 | for (int i = 0; i < points.size() - 1; i++) { |
579 | qreal x1, y1, x2, y2; |
580 | if (i == 0) { |
581 | auto element1 = group->painterPath.elementAt(i: 0); |
582 | auto element2 = group->painterPath.elementAt(i: isLine ? 1 : 3); |
583 | x1 = isHNegative ? element2.x : element1.x; |
584 | y1 = element1.y; |
585 | x2 = isHNegative ? element1.x : element2.x; |
586 | y2 = element2.y; |
587 | } else { |
588 | bool n = isVNegative | isHNegative; |
589 | // Each Spline (cubicTo) has 3 elements where third one is the x & y. |
590 | // So content of elements are: |
591 | // With Spline: |
592 | // [0] : MoveToElement |
593 | // [1] : 1. CurveToElement (c1x, c1y) |
594 | // [2] : 1. CurveToDataElement (c2x, c2y) |
595 | // [3] : 1. CurveToDataElement (x, y) |
596 | // [4] : 2. CurveToElement (c1x, c1y) |
597 | // ... |
598 | // With Line: |
599 | // [0] : MoveToElement |
600 | // [1] : 1. LineToElement (x, y) |
601 | // [2] : 2. LineToElement (x, y) |
602 | // ... |
603 | int element1Index = n ? (i + 1) : i; |
604 | int element2Index = n ? i : (i + 1); |
605 | element1Index = isLine ? element1Index : element1Index * 3; |
606 | element2Index = isLine ? element2Index : element2Index * 3; |
607 | auto element1 = group->painterPath.elementAt(i: element1Index); |
608 | auto element2 = group->painterPath.elementAt(i: element2Index); |
609 | x1 = element1.x; |
610 | y1 = element1.y; |
611 | x2 = element2.x; |
612 | y2 = element2.y; |
613 | } |
614 | |
615 | if (isLine) { |
616 | qreal denominator = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); |
617 | qreal hoverDistance = qAbs(t: (x2 - x1) * (y1 - y0) - (x1 - x0) * (y2 - y1)) |
618 | / qSqrt(v: denominator); |
619 | |
620 | if (hoverDistance < hoverSize) { |
621 | qreal alpha = 0; |
622 | qreal extrapolation = 0; |
623 | if (x2 - x1 >= y2 - y1) { |
624 | if (x2 - x1 != 0) { |
625 | alpha = ((x2 - x1) - (x0 - x1)) / qAbs(t: x2 - x1); |
626 | extrapolation = hoverSize / qAbs(t: x2 - x1); |
627 | } |
628 | } else { |
629 | if (y2 - y1 != 0) { |
630 | alpha = ((y2 - y1) - (y0 - y1)) / qAbs(t: y2 - y1); |
631 | extrapolation = hoverSize / qAbs(t: y2 - y1); |
632 | } |
633 | } |
634 | |
635 | if (alpha >= -extrapolation && alpha <= 1.0 + extrapolation) { |
636 | bool n = isVNegative | isHNegative; |
637 | |
638 | const QPointF &point1 = points[n ? i + 1 : i]; |
639 | const QPointF &point2 = points[n ? i : i + 1]; |
640 | |
641 | QPointF point = (point2 * (1.0 - alpha)) + (point1 * alpha); |
642 | |
643 | if (!group->hover) { |
644 | group->hover = true; |
645 | emit group->series->hoverEnter(seriesName: name, position, value: point); |
646 | } |
647 | |
648 | emit group->series->hover(seriesName: name, position, value: point); |
649 | hovering = true; |
650 | handled = true; |
651 | } |
652 | } |
653 | } else { // Spline |
654 | auto segments = subpath[0]; |
655 | |
656 | for (auto it = segments.begin(); it != segments.end(); ++it) { |
657 | if (std::next(x: it, n: 1) == segments.end()) |
658 | break; |
659 | |
660 | auto it2 = std::next(x: it, n: 1); |
661 | |
662 | qreal denominator = (it2->x() - it->x()) * (it2->x() - it->x()) |
663 | + (it2->y() - it->y()) * (it2->y() - it->y()); |
664 | qreal hoverDistance = qAbs(t: (it2->x() - it->x()) * (it->y() - y0) |
665 | - (it->x() - x0) * (it2->y() - it->y())) |
666 | / qSqrt(v: denominator); |
667 | |
668 | if (hoverDistance < hoverSize) { |
669 | qreal alpha = 0; |
670 | qreal extrapolation = 0; |
671 | if (it2->x() - it->x() >= it2->y() - it->y()) { |
672 | if (it2->x() - it->x() != 0) { |
673 | alpha = ((it2->x() - it->x()) - (x0 - it->x())) |
674 | / qAbs(t: it2->x() - it->x()); |
675 | extrapolation = hoverSize / qAbs(t: it2->x() - it->x()); |
676 | } |
677 | } else { |
678 | if (it2->y() - it->y() != 0) { |
679 | alpha = ((it2->y() - it->y()) - (y0 - it->y())) |
680 | / qAbs(t: it2->y() - it->y()); |
681 | extrapolation = hoverSize / qAbs(t: it2->y() - it->y()); |
682 | } |
683 | } |
684 | |
685 | if (alpha >= -extrapolation && alpha <= 1.0 + extrapolation) { |
686 | qreal cx1, cy1, cx2, cy2; |
687 | |
688 | reverseRenderCoordinates(axisRenderer, |
689 | renderX: it->x(), |
690 | renderY: it->y(), |
691 | origX: &cx1, |
692 | origY: &cy1); |
693 | reverseRenderCoordinates(axisRenderer, |
694 | renderX: it2->x(), |
695 | renderY: it2->y(), |
696 | origX: &cx2, |
697 | origY: &cy2); |
698 | |
699 | const QPointF &point1 = {cx1, cy1}; |
700 | const QPointF &point2 = {cx2, cy2}; |
701 | |
702 | QPointF point = (point2 * (1.0 - alpha)) + (point1 * alpha); |
703 | |
704 | if (!group->hover) { |
705 | group->hover = true; |
706 | emit group->series->hoverEnter(seriesName: name, position, value: point); |
707 | } |
708 | |
709 | emit group->series->hover(seriesName: name, position, value: point); |
710 | hovering = true; |
711 | handled = true; |
712 | } |
713 | } |
714 | } |
715 | } |
716 | } |
717 | |
718 | if (!hovering && group->hover) { |
719 | group->hover = false; |
720 | emit group->series->hoverExit(seriesName: name, position); |
721 | handled = true; |
722 | } |
723 | } |
724 | } |
725 | } |
726 | return handled; |
727 | } |
728 | |
729 | QT_END_NAMESPACE |
730 |
Definitions
- TAG_POINT_COLOR
- TAG_POINT_BORDER_COLOR
- TAG_POINT_BORDER_WIDTH
- TAG_POINT_SELECTED_COLOR
- TAG_POINT_SELECTED
- TAG_POINT_VALUE_X
- TAG_POINT_VALUE_Y
- PointRenderer
- ~PointRenderer
- defaultSize
- calculateRenderCoordinates
- reverseRenderCoordinates
- updatePointDelegate
- hidePointDelegates
- updateLegendData
- updateScatterSeries
- updateLineSeries
- updateSplineSeries
- handlePolish
- afterPolish
- updateSeries
- afterUpdate
- handleMouseMove
- handleMousePress
- handleMouseRelease
Start learning QML with our Intro Training
Find out more