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