1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#ifdef USE_LINEGRAPH
5#include <QtGraphs/qlineseries.h>
6#endif
7#ifdef USE_SCATTERGRAPH
8#include <QtGraphs/qscatterseries.h>
9#endif
10#ifdef USE_SPLINEGRAPH
11#include <QtGraphs/qsplineseries.h>
12#endif
13#include <QtQuick/private/qquickdraghandler_p.h>
14#include <QtQuick/private/qquicktaphandler_p.h>
15#include <private/axisrenderer_p.h>
16#include <private/pointrenderer_p.h>
17#include <private/qabstractseries_p.h>
18#include <private/qgraphsview_p.h>
19#include <private/qxyseries_p.h>
20
21QT_BEGIN_NAMESPACE
22
23static const char *TAG_POINT_COLOR = "pointColor";
24static const char *TAG_POINT_BORDER_COLOR = "pointBorderColor";
25static const char *TAG_POINT_BORDER_WIDTH = "pointBorderWidth";
26static const char *TAG_POINT_SELECTED_COLOR = "pointSelectedColor";
27static const char *TAG_POINT_SELECTED = "pointSelected";
28static const char *TAG_POINT_VALUE_X = "pointValueX";
29static const char *TAG_POINT_VALUE_Y = "pointValueY";
30static const char *TAG_POINT_INDEX = "pointIndex";
31
32PointRenderer::PointRenderer(QGraphsView *graph, bool clipPlotArea)
33 : QQuickItem(graph)
34 , m_graph(graph)
35{
36 setFlag(flag: QQuickItem::ItemHasContents);
37 setClip(clipPlotArea);
38
39 m_shape.setParentItem(this);
40 m_shape.setPreferredRendererType(QQuickShape::CurveRenderer);
41
42 const QString qmlData = QLatin1StringView(R"QML(
43 import QtQuick;
44
45 Rectangle {
46 property bool pointSelected
47 property color pointColor
48 property color pointBorderColor
49 property color pointSelectedColor
50 property real pointBorderWidth
51 color: pointSelected ? pointSelectedColor : pointColor
52 border.color: pointBorderColor
53 border.width: pointBorderWidth
54 width: %1
55 height: %1
56 }
57 )QML").arg(args: QString::number((int) defaultSize()));
58 m_tempMarker = new QQmlComponent(qmlEngine(m_graph), this);
59 m_tempMarker->setData(qmlData.toUtf8(), baseUrl: QUrl());
60
61 m_tapHandler = new QQuickTapHandler(this);
62 connect(sender: m_tapHandler, signal: &QQuickTapHandler::singleTapped,
63 context: this, slot: &PointRenderer::onSingleTapped);
64 connect(sender: m_tapHandler, signal: &QQuickTapHandler::doubleTapped,
65 context: this, slot: &PointRenderer::onDoubleTapped);
66 connect(sender: m_tapHandler, signal: &QQuickTapHandler::pressedChanged,
67 context: this, slot: &PointRenderer::onPressedChanged);
68}
69
70PointRenderer::~PointRenderer()
71{
72 qDeleteAll(c: m_groups);
73}
74
75void PointRenderer::resetShapePathCount()
76{
77 m_currentShapePathIndex = 0;
78}
79
80qreal PointRenderer::defaultSize(QXYSeries *series)
81{
82 qreal size = 16.0;
83 if (series != nullptr) {
84#ifdef USE_LINEGRAPH
85 if (auto line = qobject_cast<QLineSeries *>(object: series))
86 size = qMax(a: size, b: line->width());
87#endif
88#if defined(USE_LINEGRAPH) && defined(USE_SPLINEGRAPH)
89 else
90#endif
91#ifdef USE_SPLINEGRAPH
92 if (auto spline = qobject_cast<QSplineSeries *>(object: series))
93 size = qMax(a: size, b: spline->width());
94#endif
95 }
96 return size;
97}
98
99void PointRenderer::calculateRenderCoordinates(AxisRenderer *axisRenderer,
100 QAbstractSeries *series,
101 qreal origX,
102 qreal origY,
103 qreal *renderX,
104 qreal *renderY)
105{
106 auto &axisX = axisRenderer->getAxisX(series);
107 auto &axisY = axisRenderer->getAxisY(series);
108
109 if (m_graph->orientation() != Qt::Vertical) {
110 std::swap(a&: origX, b&: origY);
111 origY = axisY.maxValue - origY;
112 }
113
114 auto flipX = axisX.maxValue < axisX.minValue ? -1 : 1;
115 auto flipY = axisY.maxValue < axisY.minValue ? -1 : 1;
116
117 *renderX = m_areaWidth * flipX * origX * m_maxHorizontal - m_horizontalOffset;
118 *renderY = m_areaHeight - m_areaHeight * flipY * origY * m_maxVertical
119 + m_verticalOffset;
120}
121
122void PointRenderer::reverseRenderCoordinates(AxisRenderer *axisRenderer,
123 QAbstractSeries *series,
124 qreal renderX,
125 qreal renderY,
126 qreal *origX,
127 qreal *origY)
128{
129 auto &axisX = axisRenderer->getAxisX(series);
130 auto &axisY = axisRenderer->getAxisY(series);
131
132 if (m_graph->orientation() != Qt::Vertical) {
133 std::swap(a&: renderX, b&: renderY);
134 renderY = m_areaHeight - renderY;
135 }
136
137 auto flipX = axisX.maxValue < axisX.minValue ? -1 : 1;
138 auto flipY = axisY.maxValue < axisY.minValue ? -1 : 1;
139
140 *origX = (renderX + m_horizontalOffset) / (m_areaWidth * flipX * m_maxHorizontal);
141 *origY = (renderY - m_areaHeight - m_verticalOffset)
142 / (-1 * m_areaHeight * flipY * m_maxVertical);
143}
144
145PointRenderer::SeriesStyle PointRenderer::getSeriesStyle(PointGroup *group)
146{
147 auto theme = m_graph->theme();
148
149 const auto &seriesColors = theme->seriesColors();
150 const auto &borderColors = theme->borderColors();
151
152 qsizetype index = group->colorIndex % seriesColors.size();
153 QColor color = group->series->color().alpha() != 0 ? group->series->color() : seriesColors.at(i: index);
154 color.setAlpha(color.alpha() * group->series->opacity());
155
156 QColor selectedColor = group->series->selectedColor().alpha() != 0
157 ? group->series->selectedColor()
158 : m_graph->theme()->singleHighlightColor();
159 selectedColor.setAlpha(selectedColor.alpha() * group->series->opacity());
160
161 index = group->colorIndex % borderColors.size();
162 QColor borderColor = borderColors.at(i: index);
163 borderColor.setAlpha(borderColor.alpha() * group->series->opacity());
164 qreal borderWidth = theme->borderWidth();
165
166 return {
167 .color: color,
168 .selectedColor: selectedColor,
169 .borderColor: borderColor,
170 .borderWidth: borderWidth
171 };
172}
173
174void PointRenderer::updatePointDelegate(
175 QXYSeries *series, PointGroup *group, qsizetype pointIndex, qreal x, qreal y)
176{
177 const auto style = getSeriesStyle(group);
178
179 auto marker = group->markers[pointIndex];
180 auto &rect = group->rects[pointIndex];
181
182 if (marker->property(name: TAG_POINT_SELECTED).isValid())
183 marker->setProperty(name: TAG_POINT_SELECTED, value: series->isPointSelected(index: pointIndex));
184 if (marker->property(name: TAG_POINT_COLOR).isValid())
185 marker->setProperty(name: TAG_POINT_COLOR, value: style.color);
186 if (marker->property(name: TAG_POINT_BORDER_COLOR).isValid())
187 marker->setProperty(name: TAG_POINT_BORDER_COLOR, value: style.borderColor);
188 if (marker->property(name: TAG_POINT_BORDER_WIDTH).isValid())
189 marker->setProperty(name: TAG_POINT_BORDER_WIDTH, value: style.borderWidth);
190 if (marker->property(name: TAG_POINT_SELECTED_COLOR).isValid())
191 marker->setProperty(name: TAG_POINT_SELECTED_COLOR, value: style.selectedColor);
192 const auto point = series->points().at(i: pointIndex);
193 if (marker->property(name: TAG_POINT_VALUE_X).isValid())
194 marker->setProperty(name: TAG_POINT_VALUE_X, value: point.x());
195 if (marker->property(name: TAG_POINT_VALUE_Y).isValid())
196 marker->setProperty(name: TAG_POINT_VALUE_Y, value: point.y());
197 if (marker->property(name: TAG_POINT_INDEX).isValid())
198 marker->setProperty(name: TAG_POINT_INDEX, value: pointIndex);
199
200 marker->setX(x - marker->width() / 2.0);
201 marker->setY(y - marker->height() / 2.0);
202 marker->setVisible(true);
203
204 rect = QRectF(x - marker->width() / 2.0,
205 y - marker->height() / 2.0,
206 marker->width(),
207 marker->height());
208}
209
210void PointRenderer::hidePointDelegates(QXYSeries *series)
211{
212 auto *group = m_groups.value(key: series);
213 if (group->currentMarker) {
214 for (int i = 0; i < group->markers.size(); ++i) {
215 auto *marker = group->markers[i];
216 marker->setVisible(false);
217 }
218 }
219 group->rects.clear();
220}
221
222void PointRenderer::updateLegendData(QXYSeries *series, QLegendData &legendData)
223{
224 QList<QLegendData> legendDataList = {legendData};
225 series->d_func()->setLegendData(legendDataList);
226}
227
228void PointRenderer::onSingleTapped(QEventPoint eventPoint, Qt::MouseButton button)
229{
230 Q_UNUSED(button)
231
232 for (auto &&group : m_groups) {
233 if (!group->series->isVisible())
234 continue;
235
236 if (!group->series->isSelectable() && !group->series->isDraggable())
237 continue;
238
239 int index = 0;
240 for (auto &&rect : group->rects) {
241 if (rect.contains(p: eventPoint.position())) {
242 emit group->series->clicked(point: group->series->at(index).toPoint());
243 return;
244 }
245 index++;
246 }
247 }
248}
249
250void PointRenderer::onDoubleTapped(QEventPoint eventPoint, Qt::MouseButton button)
251{
252 Q_UNUSED(button)
253
254 for (auto &&group : m_groups) {
255 if (!group->series->isVisible())
256 continue;
257
258 if (!group->series->isSelectable() && !group->series->isDraggable())
259 continue;
260
261 int index = 0;
262 for (auto &&rect : group->rects) {
263 if (rect.contains(p: eventPoint.position())) {
264 emit group->series->doubleClicked(point: group->series->at(index).toPoint());
265 return;
266 }
267 index++;
268 }
269 }
270}
271
272void PointRenderer::onPressedChanged()
273{
274 if (m_tapHandler->isPressed()) {
275 for (auto &&group : m_groups) {
276 if (!group->series->isVisible())
277 continue;
278
279 if (!group->series->isSelectable() && !group->series->isDraggable())
280 continue;
281
282 int index = 0;
283 for (auto &&rect : group->rects) {
284 if (rect.contains(p: m_tapHandler->point().position())) {
285 m_pressedGroup = group;
286 m_pressedPointIndex = index;
287 emit group->series->pressed(point: m_pressedGroup->series->at(index).toPoint());
288 }
289 index++;
290 }
291 }
292 } else {
293 if (m_pressedGroup
294 && m_pressedGroup->series->isSelectable()
295 && m_pressedGroup->series->isVisible()) {
296 if (m_pressedGroup->rects[m_pressedPointIndex].contains(
297 p: m_tapHandler->point().position())) {
298 if (m_pressedGroup->series->isPointSelected(index: m_pressedPointIndex))
299 m_pressedGroup->series->deselectPoint(index: m_pressedPointIndex);
300 else
301 m_pressedGroup->series->selectPoint(index: m_pressedPointIndex);
302 m_previousDelta = QPoint(0, 0);
303 emit m_pressedGroup->series->released(
304 point: m_pressedGroup->series->at(index: m_pressedPointIndex).toPoint());
305 }
306 }
307 }
308}
309
310#ifdef USE_SCATTERGRAPH
311void PointRenderer::updateScatterSeries(QScatterSeries *series, QLegendData &legendData)
312{
313 auto group = m_groups.value(key: series);
314 const auto style = getSeriesStyle(group);
315
316 if (series->isVisible()) {
317 auto &&points = series->points();
318 group->rects.resize(size: points.size());
319 for (int i = 0; i < points.size(); ++i) {
320 qreal x, y;
321 calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer,
322 series,
323 origX: points[i].x(),
324 origY: points[i].y(),
325 renderX: &x,
326 renderY: &y);
327 y *= series->valuesMultiplier();
328 if (group->currentMarker) {
329 updatePointDelegate(series, group, pointIndex: i, x, y);
330 } else {
331 auto &rect = group->rects[i];
332 qreal size = defaultSize(series);
333 rect = QRectF(x - size / 2.0, y - size / 2.0, size, size);
334 }
335 }
336 } else {
337 hidePointDelegates(series);
338 }
339
340 legendData = { .color: style.color, .borderColor: style.borderColor, .label: series->name() };
341}
342#endif
343
344#ifdef USE_LINEGRAPH
345void PointRenderer::updateLineSeries(QLineSeries *series, QLegendData &legendData)
346{
347 auto group = m_groups.value(key: series);
348 const auto style = getSeriesStyle(group);
349
350 group->shapePath->setStrokeColor(style.color);
351 group->shapePath->setStrokeWidth(series->width());
352 group->shapePath->setFillColor(QColorConstants::Transparent);
353
354 Qt::PenCapStyle capStyle = series->capStyle();
355 if (capStyle == Qt::PenCapStyle::SquareCap)
356 group->shapePath->setCapStyle(QQuickShapePath::CapStyle::SquareCap);
357 else if (capStyle == Qt::PenCapStyle::FlatCap)
358 group->shapePath->setCapStyle(QQuickShapePath::CapStyle::FlatCap);
359 else if (capStyle == Qt::PenCapStyle::RoundCap)
360 group->shapePath->setCapStyle(QQuickShapePath::CapStyle::RoundCap);
361
362 auto &painterPath = group->painterPath;
363 painterPath.clear();
364
365 if (series->isVisible()) {
366 auto &&points = series->points();
367 group->rects.resize(size: points.size());
368 for (int i = 0; i < points.size(); ++i) {
369 qreal x, y;
370 calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer,
371 series,
372 origX: points[i].x(),
373 origY: points[i].y(),
374 renderX: &x,
375 renderY: &y);
376 y *= series->valuesMultiplier();
377 if (i == 0)
378 painterPath.moveTo(x, y);
379 else
380 painterPath.lineTo(x, y);
381
382 if (group->currentMarker) {
383 updatePointDelegate(series, group, pointIndex: i, x, y);
384 } else {
385 auto &rect = group->rects[i];
386 qreal size = defaultSize(series);
387 rect = QRectF(x - size / 2.0, y - size / 2.0, size, size);
388 }
389 }
390 } else {
391 hidePointDelegates(series);
392 }
393 group->shapePath->setPath(painterPath);
394 legendData = { .color: style.color, .borderColor: style.borderColor, .label: series->name() };
395}
396#endif
397
398#ifdef USE_SPLINEGRAPH
399void PointRenderer::updateSplineSeries(QSplineSeries *series, QLegendData &legendData)
400{
401 auto group = m_groups.value(key: series);
402 const auto style = getSeriesStyle(group);
403
404 group->shapePath->setStrokeColor(style.color);
405 group->shapePath->setStrokeWidth(series->width());
406 group->shapePath->setFillColor(QColorConstants::Transparent);
407
408 Qt::PenCapStyle capStyle = series->capStyle();
409 if (capStyle == Qt::PenCapStyle::SquareCap)
410 group->shapePath->setCapStyle(QQuickShapePath::CapStyle::SquareCap);
411 else if (capStyle == Qt::PenCapStyle::FlatCap)
412 group->shapePath->setCapStyle(QQuickShapePath::CapStyle::FlatCap);
413 else if (capStyle == Qt::PenCapStyle::RoundCap)
414 group->shapePath->setCapStyle(QQuickShapePath::CapStyle::RoundCap);
415
416 auto &painterPath = group->painterPath;
417 painterPath.clear();
418
419 if (series->isVisible()) {
420 auto &&points = series->points();
421 group->rects.resize(size: points.size());
422 auto fittedPoints = series->getControlPoints();
423
424 for (int i = 0, j = 0; i < points.size(); ++i, ++j) {
425 qreal x, y;
426 calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer,
427 series,
428 origX: points[i].x(),
429 origY: points[i].y(),
430 renderX: &x,
431 renderY: &y);
432
433 qreal valuesMultiplier = series->valuesMultiplier();
434 y *= valuesMultiplier;
435 if (i == 0) {
436 painterPath.moveTo(x, y);
437 } else {
438 qreal x1, y1, x2, y2;
439 calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer,
440 series,
441 origX: fittedPoints[j - 1].x(),
442 origY: fittedPoints[j - 1].y(),
443 renderX: &x1,
444 renderY: &y1);
445 calculateRenderCoordinates(axisRenderer: m_graph->m_axisRenderer,
446 series,
447 origX: fittedPoints[j].x(),
448 origY: fittedPoints[j].y(),
449 renderX: &x2,
450 renderY: &y2);
451
452 y1 *= valuesMultiplier;
453 y2 *= valuesMultiplier;
454 painterPath.cubicTo(ctrlPt1x: x1, ctrlPt1y: y1, ctrlPt2x: x2, ctrlPt2y: y2, endPtx: x, endPty: y);
455 ++j;
456 }
457
458 if (group->currentMarker) {
459 updatePointDelegate(series, group, pointIndex: i, x, y);
460 } else {
461 auto &rect = group->rects[i];
462 qreal size = defaultSize(series);
463 rect = QRectF(x - size / 2.0, y - size / 2.0, size, size);
464 }
465 }
466 } else {
467 hidePointDelegates(series);
468 }
469
470 group->shapePath->setPath(painterPath);
471 legendData = { .color: style.color, .borderColor: style.borderColor, .label: series->name() };
472}
473#endif
474
475void PointRenderer::handlePolish(QXYSeries *series)
476{
477 auto theme = m_graph->theme();
478 if (!theme) {
479 qCCritical(lcCritical2D, "Theme not found");
480 return;
481 }
482
483 if (!m_graph->m_axisRenderer) {
484 qCCritical(lcCritical2D, "Axis renderer not found.");
485 return;
486 }
487
488 if (series->points().isEmpty()) {
489 auto group = m_groups.value(key: series);
490
491 if (group) {
492 if (group->shapePath) {
493 auto &painterPath = group->painterPath;
494 painterPath.clear();
495 group->shapePath->setPath(painterPath);
496 }
497
498 for (auto m : std::as_const(t&: group->markers))
499 m->deleteLater();
500
501 group->markers.clear();
502 }
503
504 return;
505 }
506
507 if (width() <= 0 || height() <= 0)
508 return;
509
510 m_areaWidth = width();
511 m_areaHeight = height();
512
513 auto &axisX = m_graph->m_axisRenderer->getAxisX(series);
514 auto &axisY = m_graph->m_axisRenderer->getAxisY(series);
515
516 m_maxVertical = axisY.valueRange > 0 ? 1.0 / axisY.valueRange : 100.0;
517 m_maxHorizontal = axisX.valueRange > 0 ? 1.0 / axisX.valueRange : 100.0;
518
519 auto vmin = axisY.minValue > axisY.maxValue ? std::abs(x: axisY.minValue) : axisY.minValue;
520 m_verticalOffset = (vmin / axisY.valueRange) * m_areaHeight;
521
522 auto hmin = axisX.minValue > axisX.maxValue ? std::abs(x: axisX.minValue) : axisX.minValue;
523 m_horizontalOffset = (hmin / axisX.valueRange) * m_areaWidth;
524
525 if (!m_groups.contains(key: series)) {
526 PointGroup *group = new PointGroup();
527 group->series = series;
528 m_groups.insert(key: series, value: group);
529
530 if (series->type() != QAbstractSeries::SeriesType::Scatter) {
531 group->shapePath = new QQuickShapePath(&m_shape);
532 group->shapePath->setAsynchronous(true);
533 auto data = m_shape.data();
534 data.append(&data, group->shapePath);
535 }
536 }
537
538 auto group = m_groups.value(key: series);
539
540 if (series->type() != QAbstractSeries::SeriesType::Scatter) {
541 auto data = m_shape.data();
542 group->shapePath = qobject_cast<QQuickShapePath *>(object: data.at(&data, m_currentShapePathIndex));
543
544 m_currentShapePathIndex++;
545 }
546
547 qsizetype pointCount = series->points().size();
548
549 if ((series->type() == QAbstractSeries::SeriesType::Scatter) && !series->pointDelegate())
550 group->currentMarker = m_tempMarker;
551 else if (series->pointDelegate())
552 group->currentMarker = series->pointDelegate();
553
554 if (group->currentMarker != group->previousMarker) {
555 for (auto &&marker : group->markers)
556 marker->deleteLater();
557 group->markers.clear();
558 }
559 group->previousMarker = group->currentMarker;
560
561 if (group->currentMarker) {
562 qsizetype markerCount = group->markers.size();
563 if (markerCount < pointCount) {
564 for (qsizetype i = markerCount; i < pointCount; ++i) {
565 QQuickItem *item = qobject_cast<QQuickItem *>(
566 o: group->currentMarker->create(context: group->currentMarker->creationContext()));
567 item->setParent(this);
568 item->setParentItem(this);
569 QQuickDragHandler *handler = new QQuickDragHandler(item);
570 handler->setEnabled(series->isDraggable());
571 connect(sender: series, signal: &QXYSeries::draggableChanged, context: this, slot: [handler, series]() {
572 handler->setEnabled(series->isDraggable());
573 });
574 group->markers << item;
575 group->dragHandlers << handler;
576
577 connect(sender: handler, signal: &QQuickDragHandler::translationChanged, context: this, slot: [&]() {
578 if (m_pressedGroup) {
579 float w = width();
580 float h = height();
581 double maxVertical
582 = axisY.valueRange > 0
583 ? 1.0 / axisY.valueRange
584 : 100.0;
585 double maxHorizontal
586 = axisX.valueRange > 0
587 ? 1.0 / axisX.valueRange
588 : 100.0;
589
590 QPoint currentDelta =
591 m_pressedGroup->dragHandlers.at(i: m_pressedPointIndex)
592 ->activeTranslation().toPoint();
593 QPoint delta = currentDelta - m_previousDelta;
594 m_previousDelta = currentDelta;
595
596 qreal deltaX = delta.x() / w / maxHorizontal;
597 qreal deltaY = -delta.y() / h / maxVertical;
598
599 QPointF point = m_pressedGroup->series->at(index: m_pressedPointIndex)
600 + QPointF(deltaX, deltaY);
601 m_pressedGroup->series->replace(index: m_pressedPointIndex, newPoint: point);
602 }
603 });
604 connect(sender: handler, signal: &QQuickDragHandler::grabChanged, context: this,
605 slot: [&](QPointingDevice::GrabTransition transition, QEventPoint point) {
606 Q_UNUSED(point)
607
608 if (transition == QPointingDevice::UngrabExclusive ||
609 transition == QPointingDevice::UngrabPassive) {
610 m_previousDelta = QPoint(0, 0);
611 }
612 });
613 }
614 } else if (markerCount > pointCount) {
615 for (qsizetype i = pointCount; i < markerCount; ++i)
616 group->markers[i]->deleteLater();
617 group->markers.resize(size: pointCount);
618 }
619 } else if (group->markers.size() > 0) {
620 for (auto &&marker : group->markers)
621 marker->deleteLater();
622 group->markers.clear();
623 }
624
625 for (auto &&marker : group->markers)
626 marker->setZ(group->series->zValue());
627
628 if (group->colorIndex < 0) {
629 group->colorIndex = m_graph->graphSeriesCount();
630 m_graph->setGraphSeriesCount(group->colorIndex + 1);
631 }
632
633 QLegendData legendData;
634#ifdef USE_SCATTERGRAPH
635 if (auto scatter = qobject_cast<QScatterSeries *>(object: series))
636 updateScatterSeries(series: scatter, legendData);
637#endif
638#if defined(USE_SCATTERGRAPH) && defined(USE_LINEGRAPH)
639 else
640#endif
641#ifdef USE_LINEGRAPH
642 if (auto line = qobject_cast<QLineSeries *>(object: series))
643 updateLineSeries(series: line, legendData);
644#endif
645#if defined(USE_LINEGRAPH) && defined(USE_SPLINEGRAPH)
646 else
647#endif
648#ifdef USE_SPLINEGRAPH
649 if (auto spline = qobject_cast<QSplineSeries *>(object: series))
650 updateSplineSeries(series: spline, legendData);
651#endif
652
653 updateLegendData(series, legendData);
654}
655
656void PointRenderer::afterPolish(QList<QAbstractSeries *> &cleanupSeries)
657{
658 for (auto series : cleanupSeries) {
659 auto xySeries = qobject_cast<QXYSeries *>(object: series);
660 if (xySeries && m_groups.contains(key: xySeries)) {
661 auto group = m_groups.value(key: xySeries);
662
663 for (auto marker : std::as_const(t&: group->markers))
664 marker->deleteLater();
665
666 if (group->shapePath) {
667 auto painterPath = group->painterPath;
668 painterPath.clear();
669 group->shapePath->setPath(painterPath);
670 }
671
672 delete group;
673 m_groups.remove(key: xySeries);
674 }
675 }
676}
677
678void PointRenderer::updateSeries(QXYSeries *series)
679{
680 Q_UNUSED(series);
681}
682
683void PointRenderer::afterUpdate(QList<QAbstractSeries *> &cleanupSeries)
684{
685 Q_UNUSED(cleanupSeries);
686}
687
688bool PointRenderer::handleHoverMove(QHoverEvent *event)
689{
690 bool handled = false;
691 const QPointF &position = event->position();
692
693 for (auto &&group : m_groups) {
694 if (!group->series->isHoverable() || !group->series->isVisible())
695 continue;
696
697 auto axisRenderer = group->series->graph()->m_axisRenderer;
698 auto &axisX = axisRenderer->getAxisX(series: group->series);
699 auto &axisY = axisRenderer->getAxisY(series: group->series);
700
701 bool isHNegative = axisX.maxValue < axisX.minValue;
702 bool isVNegative = axisY.maxValue < axisY.minValue;
703
704 if (group->series->type() == QAbstractSeries::SeriesType::Scatter) {
705 const QString &name = group->series->name();
706
707 bool hovering = false;
708
709 int index = 0;
710 for (auto &&rect : group->rects) {
711 if (rect.contains(p: position.toPoint())) {
712 if (!group->hover) {
713 group->hover = true;
714 emit group->series->hoverEnter(seriesName: name, position, value: group->series->at(index));
715 }
716 emit group->series->hover(seriesName: name, position, value: group->series->at(index));
717 hovering = true;
718 }
719 index++;
720 }
721
722 if (!hovering && group->hover) {
723 group->hover = false;
724 emit group->series->hoverExit(seriesName: name, position);
725 }
726 } else {
727 const qreal x0 = event->position().x();
728 const qreal y0 = event->position().y();
729
730 const qreal hoverSize = defaultSize(series: group->series) / 2.0;
731 const QString &name = group->series->name();
732 auto &&points = group->series->points();
733 // True when line, false when spline
734 const bool isLine = group->series->type() == QAbstractSeries::SeriesType::Line;
735 if (points.size() >= 2) {
736 bool hovering = false;
737 auto subpath = group->painterPath.toSubpathPolygons();
738
739 if (group->painterPath.elementCount() != points.size())
740 m_graph->ensurePolished();
741
742 for (int i = 0; i < points.size() - 1; i++) {
743 qreal x1, y1, x2, y2;
744 if (i == 0) {
745 auto element1 = group->painterPath.elementAt(i: 0);
746 auto element2 = group->painterPath.elementAt(i: isLine ? 1 : 3);
747 x1 = isHNegative ? element2.x : element1.x;
748 y1 = element1.y;
749 x2 = isHNegative ? element1.x : element2.x;
750 y2 = element2.y;
751 } else {
752 bool n = isVNegative | isHNegative;
753 // Each Spline (cubicTo) has 3 elements where third one is the x & y.
754 // So content of elements are:
755 // With Spline:
756 // [0] : MoveToElement
757 // [1] : 1. CurveToElement (c1x, c1y)
758 // [2] : 1. CurveToDataElement (c2x, c2y)
759 // [3] : 1. CurveToDataElement (x, y)
760 // [4] : 2. CurveToElement (c1x, c1y)
761 // ...
762 // With Line:
763 // [0] : MoveToElement
764 // [1] : 1. LineToElement (x, y)
765 // [2] : 2. LineToElement (x, y)
766 // ...
767 int element1Index = n ? (i + 1) : i;
768 int element2Index = n ? i : (i + 1);
769 element1Index = isLine ? element1Index : element1Index * 3;
770 element2Index = isLine ? element2Index : element2Index * 3;
771 auto element1 = group->painterPath.elementAt(i: element1Index);
772 auto element2 = group->painterPath.elementAt(i: element2Index);
773 x1 = element1.x;
774 y1 = element1.y;
775 x2 = element2.x;
776 y2 = element2.y;
777 }
778
779 if (isLine) {
780 qreal denominator = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
781 qreal hoverDistance = qAbs(t: (x2 - x1) * (y1 - y0) - (x1 - x0) * (y2 - y1))
782 / qSqrt(v: denominator);
783
784 if (hoverDistance < hoverSize) {
785 qreal alpha = 0;
786 qreal extrapolation = 0;
787 if (x2 - x1 >= y2 - y1) {
788 if (x2 - x1 != 0) {
789 alpha = ((x2 - x1) - (x0 - x1)) / qAbs(t: x2 - x1);
790 extrapolation = hoverSize / qAbs(t: x2 - x1);
791 }
792 } else {
793 if (y2 - y1 != 0) {
794 alpha = ((y2 - y1) - (y0 - y1)) / qAbs(t: y2 - y1);
795 extrapolation = hoverSize / qAbs(t: y2 - y1);
796 }
797 }
798
799 if (alpha >= -extrapolation && alpha <= 1.0 + extrapolation) {
800 bool n = isVNegative | isHNegative;
801
802 const QPointF &point1 = points[n ? i + 1 : i];
803 const QPointF &point2 = points[n ? i : i + 1];
804
805 QPointF point = (point2 * (1.0 - alpha)) + (point1 * alpha);
806
807 if (!group->hover) {
808 group->hover = true;
809 group->series->setHovered(true);
810 emit group->series->hoverEnter(seriesName: name, position, value: point);
811 }
812
813 emit group->series->hover(seriesName: name, position, value: point);
814 hovering = true;
815 handled = true;
816 }
817 }
818 } else { // Spline
819 auto segments = subpath[0];
820
821 for (auto it = segments.begin(); it != segments.end(); ++it) {
822 if (std::next(x: it, n: 1) == segments.end())
823 break;
824
825 auto it2 = std::next(x: it, n: 1);
826
827 qreal denominator = (it2->x() - it->x()) * (it2->x() - it->x())
828 + (it2->y() - it->y()) * (it2->y() - it->y());
829 qreal hoverDistance = qAbs(t: (it2->x() - it->x()) * (it->y() - y0)
830 - (it->x() - x0) * (it2->y() - it->y()))
831 / qSqrt(v: denominator);
832
833 if (hoverDistance < hoverSize) {
834 qreal alpha = 0;
835 qreal extrapolation = 0;
836 if (it2->x() - it->x() >= it2->y() - it->y()) {
837 if (it2->x() - it->x() != 0) {
838 alpha = ((it2->x() - it->x()) - (x0 - it->x()))
839 / qAbs(t: it2->x() - it->x());
840 extrapolation = hoverSize / qAbs(t: it2->x() - it->x());
841 }
842 } else {
843 if (it2->y() - it->y() != 0) {
844 alpha = ((it2->y() - it->y()) - (y0 - it->y()))
845 / qAbs(t: it2->y() - it->y());
846 extrapolation = hoverSize / qAbs(t: it2->y() - it->y());
847 }
848 }
849
850 if (alpha >= -extrapolation && alpha <= 1.0 + extrapolation) {
851 qreal cx1, cy1, cx2, cy2;
852
853 reverseRenderCoordinates(axisRenderer,
854 series: group->series,
855 renderX: it->x(),
856 renderY: it->y(),
857 origX: &cx1,
858 origY: &cy1);
859 reverseRenderCoordinates(axisRenderer,
860 series: group->series,
861 renderX: it2->x(),
862 renderY: it2->y(),
863 origX: &cx2,
864 origY: &cy2);
865
866 const QPointF &point1 = {cx1, cy1};
867 const QPointF &point2 = {cx2, cy2};
868
869 QPointF point = (point2 * (1.0 - alpha)) + (point1 * alpha);
870
871 if (!group->hover) {
872 group->hover = true;
873 emit group->series->hoverEnter(seriesName: name, position, value: point);
874 }
875
876 emit group->series->hover(seriesName: name, position, value: point);
877 hovering = true;
878 handled = true;
879 }
880 }
881 }
882 }
883 }
884
885 if (!hovering && group->hover) {
886 group->hover = false;
887 group->series->setHovered(false);
888 emit group->series->hoverExit(seriesName: name, position);
889 handled = true;
890 }
891 }
892 }
893 }
894 return handled;
895}
896
897
898QPointF PointRenderer::reverseRenderCoordinates(QAbstractSeries *series, qreal x, qreal y)
899{
900 m_areaWidth = width();
901 m_areaHeight = height();
902
903 auto &axisX = m_graph->m_axisRenderer->getAxisX(series);
904 auto &axisY = m_graph->m_axisRenderer->getAxisY(series);
905
906 m_maxVertical = axisY.valueRange > 0 ? 1.0 / axisY.valueRange : 100.0;
907 m_maxHorizontal = axisX.valueRange > 0 ? 1.0 / axisX.valueRange : 100.0;
908
909 auto vmin = axisY.minValue > axisY.maxValue ? std::abs(x: axisY.minValue) : axisY.minValue;
910 m_verticalOffset = (vmin / axisY.valueRange) * m_areaHeight;
911
912 auto hmin = axisX.minValue > axisX.maxValue ? std::abs(x: axisX.minValue) : axisX.minValue;
913 m_horizontalOffset = (hmin / axisX.valueRange) * m_areaWidth;
914
915 qreal x0;
916 qreal y0;
917
918 reverseRenderCoordinates(axisRenderer: m_graph->m_axisRenderer, series, renderX: x, renderY: y, origX: &x0, origY: &y0);
919
920 return QPointF(x0, y0);
921}
922
923QT_END_NAMESPACE
924

source code of qtgraphs/src/graphs2d/qsgrenderer/pointrenderer.cpp