1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <QtGraphs/qareaseries.h>
5#include <QtGraphs/qsplineseries.h>
6#include <private/arearenderer_p.h>
7#include <private/pointrenderer_p.h>
8#include <private/axisrenderer_p.h>
9#include <private/qabstractseries_p.h>
10#include <private/qareaseries_p.h>
11#include <private/qgraphsview_p.h>
12#include <private/qxyseries_p.h>
13#include <QtQuick/private/qquicktaphandler_p.h>
14
15QT_BEGIN_NAMESPACE
16
17AreaRenderer::AreaRenderer(QGraphsView *graph, bool clipPlotArea)
18 : QQuickItem(graph)
19 , m_graph(graph)
20{
21 setFlag(flag: QQuickItem::ItemHasContents);
22 setClip(clipPlotArea);
23 m_shape.setParentItem(this);
24 m_shape.setPreferredRendererType(QQuickShape::CurveRenderer);
25
26 m_tapHandler = new QQuickTapHandler(this);
27 connect(sender: m_tapHandler, signal: &QQuickTapHandler::singleTapped, context: this, slot: &AreaRenderer::onSingleTapped);
28 connect(sender: m_tapHandler, signal: &QQuickTapHandler::doubleTapped, context: this, slot: &AreaRenderer::onDoubleTapped);
29 connect(sender: m_tapHandler, signal: &QQuickTapHandler::pressedChanged, context: this, slot: &AreaRenderer::onPressedChanged);
30}
31
32AreaRenderer::~AreaRenderer()
33{
34 qDeleteAll(c: m_groups);
35}
36
37void AreaRenderer::resetShapePathCount()
38{
39 m_currentShapePathIndex = 0;
40}
41
42void AreaRenderer::calculateRenderCoordinates(
43 QAreaSeries *series, qreal origX, qreal origY, qreal *renderX, qreal *renderY) const
44{
45 auto &axY = m_graph->m_axisRenderer->getAxisY(series);
46
47 if (m_graph->orientation() != Qt::Vertical) {
48 std::swap(a&: origX, b&: origY);
49 origY = axY.maxValue - origY;
50 }
51
52 *renderX = m_areaWidth * origX * m_maxHorizontal - m_horizontalOffset;
53 *renderY = m_areaHeight - m_areaHeight * origY * m_maxVertical
54 + m_verticalOffset;
55}
56
57void AreaRenderer::calculateAxisCoordinates(
58 QAreaSeries *series, qreal origX, qreal origY, qreal *axisX, qreal *axisY) const
59{
60 auto &axY = m_graph->m_axisRenderer->getAxisY(series);
61
62 if (m_graph->orientation() != Qt::Vertical) {
63 std::swap(a&: origX, b&: origY);
64 origY = axY.maxValue - origY;
65 }
66
67 *axisX = origX / m_areaWidth / m_maxHorizontal;
68 *axisY = axY.valueRange - origY / m_areaHeight / m_maxVertical;
69}
70
71void AreaRenderer::handlePolish(QAreaSeries *series)
72{
73 auto theme = m_graph->theme();
74 if (!theme) {
75 qCCritical(lcCritical2D, "theme not found.");
76 return;
77 }
78
79 if (!m_graph->m_axisRenderer) {
80 qCCritical(lcCritical2D, "axis renderer not found.");
81 return;
82 }
83
84 QXYSeries *upper = series->upperSeries();
85 QXYSeries *lower = series->lowerSeries();
86
87 if (!upper) {
88 qCCritical(lcCritical2D, "upperSeries not found.");
89 return;
90 }
91
92 if (!m_groups.contains(key: series)) {
93 PointGroup *group = new PointGroup();
94 group->series = series;
95 m_groups.insert(key: series, value: group);
96
97 group->shapePath = new QQuickShapePath(&m_shape);
98 auto data = m_shape.data();
99 data.append(&data, m_groups.value(key: series)->shapePath);
100 }
101
102 auto group = m_groups.value(key: series);
103
104 auto data = m_shape.data();
105 group->shapePath = qobject_cast<QQuickShapePath *>(object: data.at(&data, m_currentShapePathIndex));
106
107 m_currentShapePathIndex++;
108
109 if (upper->points().count() < 2 || (lower && lower->points().count() < 2)) {
110 auto painterPath = group->painterPath;
111 painterPath.clear();
112 group->shapePath->setPath(painterPath);
113 return;
114 }
115
116 m_areaWidth = width();
117 m_areaHeight = height();
118
119 auto &axisX = m_graph->m_axisRenderer->getAxisX(series: group->series);
120 auto &axisY = m_graph->m_axisRenderer->getAxisY(series: group->series);
121
122 m_maxVertical = axisY.valueRange > 0 ? 1.0 / axisY.valueRange : 100.0;
123 m_maxHorizontal = axisX.valueRange > 0 ? 1.0 / axisX.valueRange : 100.0;
124 m_verticalOffset = (axisY.minValue / axisY.valueRange) * m_areaHeight;
125 m_horizontalOffset = (axisX.minValue / axisX.valueRange) * m_areaWidth;
126
127 auto &painterPath = group->painterPath;
128 painterPath.clear();
129
130 if (group->colorIndex < 0) {
131 group->colorIndex = m_graph->graphSeriesCount();
132 m_graph->setGraphSeriesCount(group->colorIndex + 1);
133 }
134
135 const auto &seriesColors = theme->seriesColors();
136 qsizetype index = group->colorIndex % seriesColors.size();
137 QColor color = series->color().alpha() != 0
138 ? series->color()
139 : seriesColors.at(i: index);
140 const auto &borderColors = theme->borderColors();
141 index = group->colorIndex % borderColors.size();
142 QColor borderColor = series->borderColor().alpha() != 0
143 ? series->borderColor()
144 : borderColors.at(i: index);
145
146 if (series->isSelected()) {
147 color = series->selectedColor().alpha() != 0 ? series->selectedColor() : color.lighter();
148 borderColor = series->selectedBorderColor().alpha() != 0 ? series->selectedBorderColor()
149 : borderColor.lighter();
150 }
151
152 qreal borderWidth = series->borderWidth();
153 if (qFuzzyCompare(p1: borderWidth, p2: qreal(-1.0)))
154 borderWidth = theme->borderWidth();
155
156 group->shapePath->setStrokeWidth(borderWidth);
157 group->shapePath->setStrokeColor(borderColor);
158 group->shapePath->setFillColor(color);
159 group->shapePath->setCapStyle(QQuickShapePath::CapStyle::SquareCap);
160
161 auto &&upperPoints = upper->points();
162 QList<QPointF> fittedPoints;
163#ifdef USE_SPLINEGRAPH
164 if (upper->type() == QAbstractSeries::SeriesType::Spline)
165 fittedPoints = qobject_cast<QSplineSeries *>(object: upper)->getControlPoints();
166#endif
167
168 int extraPointCount = lower ? 0 : 3;
169
170 if (series->isVisible()) {
171 qreal prevUpperY = 0;
172 for (int i = 0, j = 0; i < upperPoints.size() + extraPointCount; ++i, ++j) {
173 qreal x;
174 qreal y;
175 qreal upperX;
176 qreal upperY;
177 if (i == upperPoints.size()) {
178 upperX = upperPoints[upperPoints.size() - 1].x();
179 upperY = 0;
180 } else if (i == upperPoints.size() + 1) {
181 upperX = upperPoints[0].x();
182 upperY = 0;
183 } else if (i == upperPoints.size() + 2) {
184 upperX = upperPoints[0].x();
185 upperY = upperPoints[0].y();
186 } else {
187 upperX = upperPoints[i].x();
188 upperY = upperPoints[i].y();
189 }
190 calculateRenderCoordinates(series, origX: upperX, origY: upperY, renderX: &x, renderY: &y);
191
192 if (i == 0) {
193 painterPath.moveTo(x, y);
194 } else {
195 if (i < upper->points().size()
196 && upper->type() == QAbstractSeries::SeriesType::Spline) {
197 qreal x1, y1, x2, y2;
198 calculateRenderCoordinates(series,
199 origX: fittedPoints[j - 1].x(),
200 origY: fittedPoints[j - 1].y(),
201 renderX: &x1,
202 renderY: &y1);
203 calculateRenderCoordinates(series,
204 origX: fittedPoints[j].x(),
205 origY: fittedPoints[j].y(),
206 renderX: &x2,
207 renderY: &y2);
208
209 painterPath.cubicTo(ctrlPt1x: x1, ctrlPt1y: y1, ctrlPt2x: x2, ctrlPt2y: y2, endPtx: x, endPty: y);
210 ++j;
211 } else {
212 painterPath.lineTo(x, y);
213 if (i != 0 && i < upper->points().size()
214 && upperY == 0 && prevUpperY == 0) {
215 painterPath.moveTo(x, y);
216 }
217 }
218 }
219 prevUpperY = upperY;
220 }
221 }
222
223 if (lower && series->isVisible()) {
224 auto &&lowerPoints = lower->points();
225 QList<QPointF> fittedPoints;
226#ifdef USE_SPLINEGRAPH
227 if (lower->type() == QAbstractSeries::SeriesType::Spline)
228 fittedPoints = qobject_cast<QSplineSeries *>(object: lower)->getControlPoints();
229#endif
230
231 for (int i = 0, j = 0; i < lowerPoints.size(); ++i, ++j) {
232 qreal x, y;
233 calculateRenderCoordinates(series,
234 origX: lowerPoints[lowerPoints.size() - 1 - i].x(),
235 origY: lowerPoints[lowerPoints.size() - 1 - i].y(),
236 renderX: &x,
237 renderY: &y);
238
239 if (i > 0 && lower->type() == QAbstractSeries::SeriesType::Spline) {
240 qreal x1, y1, x2, y2;
241 calculateRenderCoordinates(series,
242 origX: fittedPoints[fittedPoints.size() - 1 - j + 1].x(),
243 origY: fittedPoints[fittedPoints.size() - 1 - j + 1].y(),
244 renderX: &x1,
245 renderY: &y1);
246 calculateRenderCoordinates(series,
247 origX: fittedPoints[fittedPoints.size() - 1 - j].x(),
248 origY: fittedPoints[fittedPoints.size() - 1 - j].y(),
249 renderX: &x2,
250 renderY: &y2);
251
252 painterPath.cubicTo(ctrlPt1x: x1, ctrlPt1y: y1, ctrlPt2x: x2, ctrlPt2y: y2, endPtx: x, endPty: y);
253 ++j;
254 } else {
255 painterPath.lineTo(x, y);
256 }
257 }
258
259 qreal x, y;
260 calculateRenderCoordinates(series, origX: upperPoints[0].x(), origY: upperPoints[0].y(), renderX: &x, renderY: &y);
261 painterPath.lineTo(x, y);
262 }
263
264 group->shapePath->setPath(painterPath);
265
266 QList<QLegendData> legendDataList = {{.color: color, .borderColor: borderColor, .label: series->name()}};
267 series->d_func()->setLegendData(legendDataList);
268}
269
270void AreaRenderer::afterPolish(QList<QAbstractSeries *> &cleanupSeries)
271{
272 for (auto series : cleanupSeries) {
273 auto areaSeries = qobject_cast<QAreaSeries *>(object: series);
274 if (areaSeries && m_groups.contains(key: areaSeries)) {
275 auto group = m_groups.value(key: areaSeries);
276
277 auto painterPath = group->painterPath;
278 painterPath.clear();
279 group->shapePath->setPath(painterPath);
280
281 delete group;
282 m_groups.remove(key: areaSeries);
283 }
284 }
285}
286
287void AreaRenderer::afterUpdate(QList<QAbstractSeries *> &cleanupSeries)
288{
289 Q_UNUSED(cleanupSeries);
290}
291
292void AreaRenderer::updateSeries(QAreaSeries *series)
293{
294 Q_UNUSED(series);
295}
296
297// Point inside triangle code from
298// https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle
299float sign(QPoint p1, QPoint p2, QPoint p3)
300{
301 return (p1.x() - p3.x()) * (p2.y() - p3.y()) - (p2.x() - p3.x()) * (p1.y() - p3.y());
302}
303
304bool pointInTriangle(QPoint pt, QPoint v1, QPoint v2, QPoint v3)
305{
306 float d1, d2, d3;
307 bool hasNeg, hasPos;
308
309 d1 = sign(p1: pt, p2: v1, p3: v2);
310 d2 = sign(p1: pt, p2: v2, p3: v3);
311 d3 = sign(p1: pt, p2: v3, p3: v1);
312
313 hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
314 hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
315
316 return !(hasNeg && hasPos);
317}
318
319bool AreaRenderer::pointInArea(QPoint pt, QAreaSeries *series) const
320{
321 QList<QPointF> upperPoints = series->upperSeries()->points();
322 QList<QPointF> lowerPoints;
323
324 if (series->lowerSeries())
325 lowerPoints = series->lowerSeries()->points();
326
327 QList<QPointF> *firstPoints = &upperPoints;
328 if (lowerPoints.size() > upperPoints.size())
329 firstPoints = &lowerPoints;
330
331 for (int i = 0; i < firstPoints->size() - 1; ++i) {
332 qreal x1, y1, x2, y2, x3, y3, x4, y4;
333 calculateRenderCoordinates(series, origX: (*firstPoints)[i].x(), origY: (*firstPoints)[i].y(), renderX: &x1, renderY: &y1);
334 calculateRenderCoordinates(series,
335 origX: (*firstPoints)[i + 1].x(),
336 origY: (*firstPoints)[i + 1].y(),
337 renderX: &x2,
338 renderY: &y2);
339
340 bool needSecondTriangleTest = true;
341 if (series->lowerSeries()) {
342 QList<QPointF> *secondPoints = &lowerPoints;
343 if (lowerPoints.size() > upperPoints.size())
344 secondPoints = &upperPoints;
345
346 qsizetype firstIndex = i;
347 qsizetype secondIndex = i + 1;
348
349 if (firstIndex >= secondPoints->size())
350 firstIndex = secondPoints->size() - 1;
351 if (secondIndex >= secondPoints->size())
352 needSecondTriangleTest = false;
353
354 calculateRenderCoordinates(series,
355 origX: (*secondPoints)[firstIndex].x(),
356 origY: (*secondPoints)[firstIndex].y(),
357 renderX: &x3,
358 renderY: &y3);
359
360 if (needSecondTriangleTest) {
361 calculateRenderCoordinates(series,
362 origX: (*secondPoints)[secondIndex].x(),
363 origY: (*secondPoints)[secondIndex].y(),
364 renderX: &x4,
365 renderY: &y4);
366 } else {
367 x4 = 0.0;
368 y4 = 0.0;
369 }
370 } else {
371 calculateRenderCoordinates(series, origX: upperPoints[i].x(), origY: 0, renderX: &x3, renderY: &y3);
372 calculateRenderCoordinates(series, origX: upperPoints[i + 1].x(), origY: 0, renderX: &x4, renderY: &y4);
373 }
374
375 QPoint point1(x1, y1);
376 QPoint point2(x2, y2);
377 QPoint point3(x3, y3);
378 QPoint point4(x4, y4);
379
380 if (pointInTriangle(pt, v1: point1, v2: point2, v3: point3)
381 || (needSecondTriangleTest && pointInTriangle(pt, v1: point2, v2: point3, v3: point4))) {
382 return true;
383 }
384 }
385
386 return false;
387}
388
389bool AreaRenderer::handleHoverMove(QHoverEvent *event)
390{
391 bool handled = false;
392 const QPointF &position = event->position();
393
394 for (auto &&group : m_groups) {
395 if (!group->series->isHoverable() || !group->series->isVisible())
396 continue;
397
398 if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2)
399 continue;
400
401 if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2)
402 continue;
403
404 const QString &name = group->series->name();
405
406 bool hovering = false;
407 if (pointInArea(pt: position.toPoint(), series: group->series)) {
408 qreal x, y;
409 calculateAxisCoordinates(series: group->series, origX: position.x(), origY: position.y(), axisX: &x, axisY: &y);
410
411 if (!group->hover) {
412 group->hover = true;
413 group->series->setHovered(true);
414 emit group->series->hoverEnter(seriesName: name, position, value: QPointF(x, y));
415 }
416
417 emit group->series->hover(seriesName: name, position, value: QPointF(x, y));
418 hovering = true;
419 handled = true;
420 }
421
422 if (!hovering && group->hover) {
423 group->hover = false;
424 group->series->setHovered(false);
425 emit group->series->hoverExit(seriesName: name, position);
426 handled = true;
427 }
428 }
429 return handled;
430}
431
432void AreaRenderer::onSingleTapped(QEventPoint eventPoint, Qt::MouseButton button)
433{
434 Q_UNUSED(button)
435
436 for (auto &&group : m_groups) {
437 if (!group->series->isSelectable() || !group->series->isVisible())
438 continue;
439
440 if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2)
441 continue;
442
443 if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2)
444 continue;
445
446 if (pointInArea(pt: eventPoint.position().toPoint(), series: group->series)) {
447 group->series->setSelected(!group->series->isSelected());
448 m_graph->polishAndUpdate();
449 qreal x;
450 qreal y;
451 calculateAxisCoordinates(series: group->series,
452 origX: eventPoint.position().x(),
453 origY: eventPoint.position().y(),
454 axisX: &x,
455 axisY: &y);
456 emit group->series->clicked(point: QPoint(x, y));
457 }
458 }
459}
460
461void AreaRenderer::onDoubleTapped(QEventPoint eventPoint, Qt::MouseButton button)
462{
463 Q_UNUSED(button)
464
465 for (auto &&group : m_groups) {
466 if (!group->series->isSelectable() || !group->series->isVisible())
467 continue;
468
469 if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2)
470 continue;
471
472 if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2)
473 continue;
474
475 if (pointInArea(pt: eventPoint.position().toPoint(), series: group->series)) {
476 qreal x;
477 qreal y;
478 calculateAxisCoordinates(series: group->series,
479 origX: eventPoint.position().x(),
480 origY: eventPoint.position().y(),
481 axisX: &x,
482 axisY: &y);
483 emit group->series->doubleClicked(point: QPoint(x, y));
484 }
485 }
486}
487
488void AreaRenderer::onPressedChanged()
489{
490 QPointF position = m_tapHandler->point().position();
491 for (auto &&group : m_groups) {
492 if (!group->series->isSelectable() || !group->series->isVisible())
493 continue;
494
495 if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2)
496 continue;
497
498 if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2)
499 continue;
500
501 if (pointInArea(pt: position.toPoint(), series: group->series)) {
502 qreal x;
503 qreal y;
504 calculateAxisCoordinates(series: group->series, origX: position.x(), origY: position.y(), axisX: &x, axisY: &y);
505 if (m_tapHandler->isPressed())
506 emit group->series->pressed(point: QPoint(x, y));
507 else
508 emit group->series->released(point: QPoint(x, y));
509 }
510 }
511}
512
513QT_END_NAMESPACE
514

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