| 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 | |
| 21 | QT_BEGIN_NAMESPACE |
| 22 | |
| 23 | static const char *TAG_POINT_COLOR = "pointColor" ; |
| 24 | static const char *TAG_POINT_BORDER_COLOR = "pointBorderColor" ; |
| 25 | static const char *TAG_POINT_BORDER_WIDTH = "pointBorderWidth" ; |
| 26 | static const char *TAG_POINT_SELECTED_COLOR = "pointSelectedColor" ; |
| 27 | static const char *TAG_POINT_SELECTED = "pointSelected" ; |
| 28 | static const char *TAG_POINT_VALUE_X = "pointValueX" ; |
| 29 | static const char *TAG_POINT_VALUE_Y = "pointValueY" ; |
| 30 | static const char *TAG_POINT_INDEX = "pointIndex" ; |
| 31 | |
| 32 | PointRenderer::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 | |
| 70 | PointRenderer::~PointRenderer() |
| 71 | { |
| 72 | qDeleteAll(c: m_groups); |
| 73 | } |
| 74 | |
| 75 | void PointRenderer::resetShapePathCount() |
| 76 | { |
| 77 | m_currentShapePathIndex = 0; |
| 78 | } |
| 79 | |
| 80 | qreal 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 | |
| 99 | void 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 | |
| 122 | void 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 | |
| 145 | PointRenderer::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 | |
| 174 | void 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 | |
| 210 | void 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 | |
| 222 | void PointRenderer::updateLegendData(QXYSeries *series, QLegendData &legendData) |
| 223 | { |
| 224 | QList<QLegendData> legendDataList = {legendData}; |
| 225 | series->d_func()->setLegendData(legendDataList); |
| 226 | } |
| 227 | |
| 228 | void 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 | |
| 250 | void 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 | |
| 272 | void 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 |
| 311 | void 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 |
| 345 | void 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 |
| 399 | void 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 | |
| 475 | void 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 | |
| 656 | void 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 | |
| 678 | void PointRenderer::updateSeries(QXYSeries *series) |
| 679 | { |
| 680 | Q_UNUSED(series); |
| 681 | } |
| 682 | |
| 683 | void PointRenderer::afterUpdate(QList<QAbstractSeries *> &cleanupSeries) |
| 684 | { |
| 685 | Q_UNUSED(cleanupSeries); |
| 686 | } |
| 687 | |
| 688 | bool 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 = 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 = 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 | |
| 898 | QPointF 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 | |
| 923 | QT_END_NAMESPACE |
| 924 | |