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 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | AreaRenderer::AreaRenderer(QGraphsView *graph) |
17 | : QQuickItem(graph) |
18 | , m_graph(graph) |
19 | { |
20 | setFlag(flag: QQuickItem::ItemHasContents); |
21 | setClip(true); |
22 | m_shape.setParentItem(this); |
23 | m_shape.setPreferredRendererType(QQuickShape::CurveRenderer); |
24 | } |
25 | |
26 | AreaRenderer::~AreaRenderer() |
27 | { |
28 | qDeleteAll(c: m_groups); |
29 | } |
30 | |
31 | void AreaRenderer::calculateRenderCoordinates(qreal origX, |
32 | qreal origY, |
33 | qreal *renderX, |
34 | qreal *renderY) const |
35 | { |
36 | *renderX = m_areaWidth * origX * m_maxHorizontal - m_horizontalOffset; |
37 | *renderY = m_areaHeight - m_areaHeight * origY * m_maxVertical |
38 | + m_verticalOffset; |
39 | } |
40 | |
41 | void AreaRenderer::calculateAxisCoordinates(qreal origX, |
42 | qreal origY, |
43 | qreal *axisX, |
44 | qreal *axisY) const |
45 | { |
46 | *axisX = origX / m_areaWidth |
47 | / m_maxHorizontal; |
48 | *axisY = m_graph->m_axisRenderer->m_axisVerticalValueRange |
49 | - origY / m_areaHeight / m_maxVertical; |
50 | } |
51 | |
52 | void AreaRenderer::handlePolish(QAreaSeries *series) |
53 | { |
54 | auto theme = m_graph->theme(); |
55 | if (!theme) |
56 | return; |
57 | |
58 | if (!m_graph->m_axisRenderer) |
59 | return; |
60 | |
61 | QXYSeries *upper = series->upperSeries(); |
62 | QXYSeries *lower = series->lowerSeries(); |
63 | |
64 | if (!upper) |
65 | return; |
66 | |
67 | if (!m_groups.contains(key: series)) { |
68 | PointGroup *group = new PointGroup(); |
69 | group->series = series; |
70 | m_groups.insert(key: series, value: group); |
71 | |
72 | group->shapePath = new QQuickShapePath(&m_shape); |
73 | auto data = m_shape.data(); |
74 | data.append(&data, m_groups.value(key: series)->shapePath); |
75 | } |
76 | |
77 | auto group = m_groups.value(key: series); |
78 | |
79 | if (upper->points().count() < 2 || (lower && lower->points().count() < 2)) { |
80 | auto painterPath = group->painterPath; |
81 | painterPath.clear(); |
82 | group->shapePath->setPath(painterPath); |
83 | return; |
84 | } |
85 | |
86 | m_areaWidth = width(); |
87 | m_areaHeight = height(); |
88 | |
89 | m_maxVertical = m_graph->m_axisRenderer->m_axisVerticalValueRange > 0 |
90 | ? 1.0 / m_graph->m_axisRenderer->m_axisVerticalValueRange |
91 | : 100.0; |
92 | m_maxHorizontal = m_graph->m_axisRenderer->m_axisHorizontalValueRange > 0 |
93 | ? 1.0 / m_graph->m_axisRenderer->m_axisHorizontalValueRange |
94 | : 100.0; |
95 | m_verticalOffset = (m_graph->m_axisRenderer->m_axisVerticalMinValue |
96 | / m_graph->m_axisRenderer->m_axisVerticalValueRange) |
97 | * m_areaHeight; |
98 | m_horizontalOffset = (m_graph->m_axisRenderer->m_axisHorizontalMinValue |
99 | / m_graph->m_axisRenderer->m_axisHorizontalValueRange) |
100 | * m_areaWidth; |
101 | |
102 | auto &painterPath = group->painterPath; |
103 | painterPath.clear(); |
104 | |
105 | if (group->colorIndex < 0) { |
106 | group->colorIndex = m_graph->graphSeriesCount(); |
107 | m_graph->setGraphSeriesCount(group->colorIndex + 1); |
108 | } |
109 | |
110 | const auto &seriesColors = theme->seriesColors(); |
111 | qsizetype index = group->colorIndex % seriesColors.size(); |
112 | QColor color = series->color().alpha() != 0 |
113 | ? series->color() |
114 | : seriesColors.at(i: index); |
115 | const auto &borderColors = theme->borderColors(); |
116 | index = group->colorIndex % borderColors.size(); |
117 | QColor borderColor = series->borderColor().alpha() != 0 |
118 | ? series->borderColor() |
119 | : borderColors.at(i: index); |
120 | |
121 | if (series->isSelected()) { |
122 | color = series->selectedColor().alpha() != 0 ? series->selectedColor() : color.lighter(); |
123 | borderColor = series->selectedBorderColor().alpha() != 0 ? series->selectedBorderColor() |
124 | : borderColor.lighter(); |
125 | } |
126 | |
127 | qreal borderWidth = series->borderWidth(); |
128 | if (qFuzzyCompare(p1: borderWidth, p2: qreal(-1.0))) |
129 | borderWidth = theme->borderWidth(); |
130 | |
131 | group->shapePath->setStrokeWidth(borderWidth); |
132 | group->shapePath->setStrokeColor(borderColor); |
133 | group->shapePath->setFillColor(color); |
134 | group->shapePath->setCapStyle(QQuickShapePath::CapStyle::SquareCap); |
135 | |
136 | auto &&upperPoints = upper->points(); |
137 | QList<QPointF> fittedPoints; |
138 | if (upper->type() == QAbstractSeries::SeriesType::Spline) |
139 | fittedPoints = qobject_cast<QSplineSeries *>(object: upper)->getControlPoints(); |
140 | |
141 | int = lower ? 0 : 3; |
142 | |
143 | if (series->isVisible()) { |
144 | for (int i = 0, j = 0; i < upperPoints.size() + extraPointCount; ++i, ++j) { |
145 | qreal x, y; |
146 | if (i == upperPoints.size()) |
147 | calculateRenderCoordinates(origX: upperPoints[upperPoints.size() - 1].x(), origY: 0, renderX: &x, renderY: &y); |
148 | else if (i == upperPoints.size() + 1) |
149 | calculateRenderCoordinates(origX: upperPoints[0].x(), origY: 0, renderX: &x, renderY: &y); |
150 | else if (i == upperPoints.size() + 2) |
151 | calculateRenderCoordinates(origX: upperPoints[0].x(), origY: upperPoints[0].y(), renderX: &x, renderY: &y); |
152 | else |
153 | calculateRenderCoordinates(origX: upperPoints[i].x(), origY: upperPoints[i].y(), renderX: &x, renderY: &y); |
154 | |
155 | if (i == 0) { |
156 | painterPath.moveTo(x, y); |
157 | } else { |
158 | if (i < upper->points().size() |
159 | && upper->type() == QAbstractSeries::SeriesType::Spline) { |
160 | qreal x1, y1, x2, y2; |
161 | calculateRenderCoordinates(origX: fittedPoints[j - 1].x(), |
162 | origY: fittedPoints[j - 1].y(), |
163 | renderX: &x1, |
164 | renderY: &y1); |
165 | calculateRenderCoordinates(origX: fittedPoints[j].x(), origY: fittedPoints[j].y(), renderX: &x2, renderY: &y2); |
166 | |
167 | painterPath.cubicTo(ctrlPt1x: x1, ctrlPt1y: y1, ctrlPt2x: x2, ctrlPt2y: y2, endPtx: x, endPty: y); |
168 | ++j; |
169 | } else { |
170 | painterPath.lineTo(x, y); |
171 | } |
172 | } |
173 | } |
174 | } |
175 | |
176 | if (lower && series->isVisible()) { |
177 | auto &&lowerPoints = lower->points(); |
178 | QList<QPointF> fittedPoints; |
179 | if (lower->type() == QAbstractSeries::SeriesType::Spline) |
180 | fittedPoints = qobject_cast<QSplineSeries *>(object: lower)->getControlPoints(); |
181 | |
182 | for (int i = 0, j = 0; i < lowerPoints.size(); ++i, ++j) { |
183 | qreal x, y; |
184 | calculateRenderCoordinates(origX: lowerPoints[lowerPoints.size() - 1 - i].x(), |
185 | origY: lowerPoints[lowerPoints.size() - 1 - i].y(), |
186 | renderX: &x, |
187 | renderY: &y); |
188 | |
189 | if (i > 0 && lower->type() == QAbstractSeries::SeriesType::Spline) { |
190 | qreal x1, y1, x2, y2; |
191 | calculateRenderCoordinates(origX: fittedPoints[fittedPoints.size() - 1 - j + 1].x(), |
192 | origY: fittedPoints[fittedPoints.size() - 1 - j + 1].y(), |
193 | renderX: &x1, |
194 | renderY: &y1); |
195 | calculateRenderCoordinates(origX: fittedPoints[fittedPoints.size() - 1 - j].x(), |
196 | origY: fittedPoints[fittedPoints.size() - 1 - j].y(), |
197 | renderX: &x2, |
198 | renderY: &y2); |
199 | |
200 | painterPath.cubicTo(ctrlPt1x: x1, ctrlPt1y: y1, ctrlPt2x: x2, ctrlPt2y: y2, endPtx: x, endPty: y); |
201 | ++j; |
202 | } else { |
203 | painterPath.lineTo(x, y); |
204 | } |
205 | } |
206 | |
207 | qreal x, y; |
208 | calculateRenderCoordinates(origX: upperPoints[0].x(), origY: upperPoints[0].y(), renderX: &x, renderY: &y); |
209 | painterPath.lineTo(x, y); |
210 | } |
211 | |
212 | group->shapePath->setPath(painterPath); |
213 | |
214 | QList<QLegendData> legendDataList = {{.color: color, .borderColor: borderColor, .label: series->name()}}; |
215 | series->d_func()->setLegendData(legendDataList); |
216 | } |
217 | |
218 | void AreaRenderer::afterPolish(QList<QAbstractSeries *> &cleanupSeries) |
219 | { |
220 | for (auto series : cleanupSeries) { |
221 | auto areaSeries = qobject_cast<QAreaSeries *>(object: series); |
222 | if (areaSeries && m_groups.contains(key: areaSeries)) { |
223 | auto group = m_groups.value(key: areaSeries); |
224 | |
225 | auto painterPath = group->painterPath; |
226 | painterPath.clear(); |
227 | group->shapePath->setPath(painterPath); |
228 | |
229 | delete group; |
230 | m_groups.remove(key: areaSeries); |
231 | } |
232 | } |
233 | } |
234 | |
235 | void AreaRenderer::afterUpdate(QList<QAbstractSeries *> &cleanupSeries) |
236 | { |
237 | Q_UNUSED(cleanupSeries); |
238 | } |
239 | |
240 | void AreaRenderer::updateSeries(QAreaSeries *series) |
241 | { |
242 | Q_UNUSED(series); |
243 | } |
244 | |
245 | // Point inside triangle code from |
246 | // https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle |
247 | float sign(QPoint p1, QPoint p2, QPoint p3) |
248 | { |
249 | return (p1.x() - p3.x()) * (p2.y() - p3.y()) - (p2.x() - p3.x()) * (p1.y() - p3.y()); |
250 | } |
251 | |
252 | bool pointInTriangle(QPoint pt, QPoint v1, QPoint v2, QPoint v3) |
253 | { |
254 | float d1, d2, d3; |
255 | bool hasNeg, hasPos; |
256 | |
257 | d1 = sign(p1: pt, p2: v1, p3: v2); |
258 | d2 = sign(p1: pt, p2: v2, p3: v3); |
259 | d3 = sign(p1: pt, p2: v3, p3: v1); |
260 | |
261 | hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0); |
262 | hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0); |
263 | |
264 | return !(hasNeg && hasPos); |
265 | } |
266 | |
267 | bool AreaRenderer::pointInArea(QPoint pt, QAreaSeries *series) const |
268 | { |
269 | QList<QPointF> upperPoints = series->upperSeries()->points(); |
270 | QList<QPointF> lowerPoints; |
271 | |
272 | if (series->lowerSeries()) |
273 | lowerPoints = series->lowerSeries()->points(); |
274 | |
275 | QList<QPointF> *firstPoints = &upperPoints; |
276 | if (lowerPoints.size() > upperPoints.size()) |
277 | firstPoints = &lowerPoints; |
278 | |
279 | for (int i = 0; i < firstPoints->size() - 1; ++i) { |
280 | qreal x1, y1, x2, y2, x3, y3, x4, y4; |
281 | calculateRenderCoordinates(origX: (*firstPoints)[i].x(), origY: (*firstPoints)[i].y(), renderX: &x1, renderY: &y1); |
282 | calculateRenderCoordinates(origX: (*firstPoints)[i + 1].x(), origY: (*firstPoints)[i + 1].y(), renderX: &x2, renderY: &y2); |
283 | |
284 | bool needSecondTriangleTest = true; |
285 | if (series->lowerSeries()) { |
286 | QList<QPointF> *secondPoints = &lowerPoints; |
287 | if (lowerPoints.size() > upperPoints.size()) |
288 | secondPoints = &upperPoints; |
289 | |
290 | qsizetype firstIndex = i; |
291 | qsizetype secondIndex = i + 1; |
292 | |
293 | if (firstIndex >= secondPoints->size()) |
294 | firstIndex = secondPoints->size() - 1; |
295 | if (secondIndex >= secondPoints->size()) |
296 | needSecondTriangleTest = false; |
297 | |
298 | calculateRenderCoordinates(origX: (*secondPoints)[firstIndex].x(), |
299 | origY: (*secondPoints)[firstIndex].y(), |
300 | renderX: &x3, |
301 | renderY: &y3); |
302 | |
303 | if (needSecondTriangleTest) { |
304 | calculateRenderCoordinates(origX: (*secondPoints)[secondIndex].x(), |
305 | origY: (*secondPoints)[secondIndex].y(), |
306 | renderX: &x4, |
307 | renderY: &y4); |
308 | } else { |
309 | x4 = 0.0; |
310 | y4 = 0.0; |
311 | } |
312 | } else { |
313 | calculateRenderCoordinates(origX: upperPoints[i].x(), origY: 0, renderX: &x3, renderY: &y3); |
314 | calculateRenderCoordinates(origX: upperPoints[i + 1].x(), origY: 0, renderX: &x4, renderY: &y4); |
315 | } |
316 | |
317 | QPoint point1(x1, y1); |
318 | QPoint point2(x2, y2); |
319 | QPoint point3(x3, y3); |
320 | QPoint point4(x4, y4); |
321 | |
322 | if (pointInTriangle(pt, v1: point1, v2: point2, v3: point3) |
323 | || (needSecondTriangleTest && pointInTriangle(pt, v1: point2, v2: point3, v3: point4))) { |
324 | return true; |
325 | } |
326 | } |
327 | |
328 | return false; |
329 | } |
330 | |
331 | bool AreaRenderer::handleMousePress(QMouseEvent *event) |
332 | { |
333 | bool handled = false; |
334 | for (auto &&group : m_groups) { |
335 | if (!group->series->isSelectable() || !group->series->isVisible()) |
336 | continue; |
337 | |
338 | if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2) |
339 | continue; |
340 | |
341 | if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2) |
342 | continue; |
343 | |
344 | if (pointInArea(pt: event->pos(), series: group->series)) { |
345 | group->series->setSelected(!group->series->isSelected()); |
346 | handled = true; |
347 | } |
348 | } |
349 | return handled; |
350 | } |
351 | |
352 | bool AreaRenderer::handleHoverMove(QHoverEvent *event) |
353 | { |
354 | bool handled = false; |
355 | const QPointF &position = event->position(); |
356 | |
357 | for (auto &&group : m_groups) { |
358 | if (!group->series->isHoverable() || !group->series->isVisible()) |
359 | continue; |
360 | |
361 | if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2) |
362 | continue; |
363 | |
364 | if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2) |
365 | continue; |
366 | |
367 | const QString &name = group->series->name(); |
368 | |
369 | bool hovering = false; |
370 | if (pointInArea(pt: position.toPoint(), series: group->series)) { |
371 | qreal x, y; |
372 | calculateAxisCoordinates(origX: position.x(), origY: position.y(), axisX: &x, axisY: &y); |
373 | |
374 | if (!group->hover) { |
375 | group->hover = true; |
376 | emit group->series->hoverEnter(seriesName: name, position, value: QPointF(x, y)); |
377 | } |
378 | |
379 | emit group->series->hover(seriesName: name, position, value: QPointF(x, y)); |
380 | hovering = true; |
381 | handled = true; |
382 | } |
383 | |
384 | if (!hovering && group->hover) { |
385 | group->hover = false; |
386 | emit group->series->hoverExit(seriesName: name, position); |
387 | handled = true; |
388 | } |
389 | } |
390 | return handled; |
391 | } |
392 | |
393 | QT_END_NAMESPACE |
394 | |