1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <QtGraphs/qpieseries.h>
5#include <QtGraphs/qpieslice.h>
6#include <QtQuick/private/qquicktext_p.h>
7#include <QtQuick/private/qquicktaphandler_p.h>
8#include <private/pierenderer_p.h>
9#include <private/qabstractseries_p.h>
10#include <private/qgraphsview_p.h>
11#include <private/qpieseries_p.h>
12#include <private/qpieslice_p.h>
13#include <private/qquickshape_p.h>
14#include <private/qquicksvgparser_p.h>
15
16QT_BEGIN_NAMESPACE
17
18PieRenderer::PieRenderer(QGraphsView *graph, bool clipPlotArea)
19 : QQuickItem(graph)
20 , m_graph(graph)
21 , m_painterPath()
22{
23 setFlag(flag: QQuickItem::ItemHasContents);
24 setClip(clipPlotArea);
25
26 m_shape = new QQuickShape(this);
27 m_shape->setParentItem(this);
28 m_shape->setPreferredRendererType(QQuickShape::CurveRenderer);
29
30 m_tapHandler = new QQuickTapHandler(this);
31 connect(sender: m_tapHandler, signal: &QQuickTapHandler::singleTapped, context: this, slot: &PieRenderer::onSingleTapped);
32 connect(sender: m_tapHandler, signal: &QQuickTapHandler::doubleTapped, context: this, slot: &PieRenderer::onDoubleTapped);
33 connect(sender: m_tapHandler, signal: &QQuickTapHandler::pressedChanged, context: this, slot: &PieRenderer::onPressedChanged);
34}
35
36PieRenderer::~PieRenderer() {}
37
38void PieRenderer::setSize(QSizeF size)
39{
40 QQuickItem::setSize(size);
41}
42
43void PieRenderer::handlePolish(QPieSeries *series)
44{
45 auto slices = series->slices();
46 for (QPieSlice *slice : std::as_const(t&: slices)) {
47 QPieSlicePrivate *d = slice->d_func();
48 QQuickShapePath *shapePath = d->m_shapePath;
49 QQuickShapePath *labelPath = d->m_labelPath;
50 auto labelElements = labelPath->pathElements();
51 auto pathElements = shapePath->pathElements();
52 auto labelItem = d->m_labelItem;
53
54 if (!m_activeSlices.contains(key: slice)) {
55 auto data = m_shape->data();
56 data.append(&data, shapePath);
57 SliceData sliceData{};
58 sliceData.initialized = false;
59 m_activeSlices.insert(key: slice, value: sliceData);
60 }
61
62 QQuickShape *labelShape = d->m_labelShape;
63 labelShape->setVisible(series->isVisible() && d->m_isLabelVisible);
64 labelItem->setVisible(series->isVisible() && d->m_isLabelVisible);
65
66 if (!series->isVisible()) {
67 pathElements.clear(&pathElements);
68 labelElements.clear(&labelElements);
69 continue;
70 }
71
72 if (!shapePath->parent())
73 shapePath->setParent(m_shape);
74
75 if (!d->m_labelItem->parent()) {
76 d->m_labelItem->setParent(this);
77 d->m_labelItem->setParentItem(this);
78 }
79
80 if (!labelShape->parent()) {
81 labelShape->setParent(this);
82 labelShape->setParentItem(this);
83 }
84 }
85
86 if (!series->isVisible())
87 return;
88
89 QPointF center = QPointF(size().width() * series->horizontalPosition(),
90 size().height() * series->verticalPosition());
91 qreal radius = size().width() > size().height() ? size().height() : size().width();
92 radius *= (.5 * series->pieSize());
93
94 QGraphsTheme *theme = m_graph->theme();
95
96 if (!theme) {
97 qCCritical(lcCritical2D, "Theme not found.");
98 return;
99 }
100
101 if (m_colorIndex < 0)
102 m_colorIndex = m_graph->graphSeriesCount();
103 m_graph->setGraphSeriesCount(m_colorIndex + series->slices().size());
104
105 qreal sliceAngle = series->startAngle();
106 int sliceIndex = 0;
107 QList<QLegendData> legendDataList;
108 auto slicelist = series->slices();
109 for (QPieSlice *slice : std::as_const(t&: slicelist)) {
110 m_painterPath.clear();
111
112 QPieSlicePrivate *d = slice->d_func();
113 d->setStartAngle(sliceAngle);
114 d->setAngleSpan((series->endAngle() - series->startAngle()) * slice->percentage()
115 * series->valuesMultiplier());
116
117 // update slice
118 QQuickShapePath *shapePath = d->m_shapePath;
119
120 const auto &borderColors = theme->borderColors();
121 int index = sliceIndex % borderColors.size();
122 QColor borderColor = borderColors.at(i: index);
123 if (d->m_borderColor.isValid())
124 borderColor = d->m_borderColor;
125 qreal borderWidth = theme->borderWidth();
126 if (d->m_borderWidth >= 1.0)
127 borderWidth = d->m_borderWidth;
128 const auto &seriesColors = theme->seriesColors();
129 index = sliceIndex % seriesColors.size();
130 QColor color = seriesColors.at(i: index);
131 if (d->m_color.isValid())
132 color = d->m_color;
133 shapePath->setStrokeWidth(borderWidth);
134 shapePath->setStrokeColor(borderColor);
135 shapePath->setFillColor(color);
136
137 QColor labelTextColor = theme->labelTextColor();
138 if (d->m_labelColor.isValid())
139 labelTextColor = d->m_labelColor;
140 d->m_labelItem->setColor(labelTextColor);
141 d->m_labelPath->setStrokeColor(labelTextColor);
142
143 if (!m_activeSlices.contains(key: slice))
144 return;
145
146 qreal radian = qDegreesToRadians(degrees: slice->startAngle());
147 qreal startBigX = radius * qSin(v: radian);
148 qreal startBigY = radius * qCos(v: radian);
149
150 qreal startSmallX = startBigX * series->holeSize();
151 qreal startSmallY = startBigY * series->holeSize();
152
153 qreal explodeDistance = .0;
154 if (slice->isExploded())
155 explodeDistance = slice->explodeDistanceFactor() * radius;
156 radian = qDegreesToRadians(degrees: slice->startAngle() + (slice->angleSpan() * .5));
157 qreal xShift = center.x() + (explodeDistance * qSin(v: radian));
158 qreal yShift = center.y() - (explodeDistance * qCos(v: radian));
159
160 qreal pointX = startBigY * qSin(v: radian) + startBigX * qCos(v: radian);
161 qreal pointY = startBigY * qCos(v: radian) - startBigX * qSin(v: radian);
162
163 QRectF rect(center.x() - radius + (explodeDistance * qSin(v: radian)),
164 center.y() - radius - (explodeDistance * qCos(v: radian)),
165 radius * 2,
166 radius * 2);
167
168 shapePath->setStartX(center.x());
169 shapePath->setStartY(center.y());
170
171 if (series->holeSize() > 0) {
172 QRectF insideRect(center.x() - series->holeSize() * radius
173 + (explodeDistance * qSin(v: radian)),
174 center.y() - series->holeSize() * radius
175 - (explodeDistance * qCos(v: radian)),
176 series->holeSize() * radius * 2,
177 series->holeSize() * radius * 2);
178
179 m_painterPath.arcMoveTo(rect, angle: -slice->startAngle() + 90);
180 m_painterPath.arcTo(rect, startAngle: -slice->startAngle() + 90, arcLength: -slice->angleSpan());
181 m_painterPath.arcTo(rect: insideRect,
182 startAngle: -slice->startAngle() + 90 - slice->angleSpan(),
183 arcLength: slice->angleSpan());
184 m_painterPath.closeSubpath();
185 } else {
186 m_painterPath.moveTo(p: rect.center());
187 m_painterPath.arcTo(rect, startAngle: -slice->startAngle() + 90, arcLength: -slice->angleSpan());
188 m_painterPath.closeSubpath();
189 }
190
191 radian = qDegreesToRadians(degrees: slice->angleSpan());
192
193 pointX = startSmallY * qSin(v: radian) + startSmallX * qCos(v: radian);
194 pointY = startSmallY * qCos(v: radian) - startSmallX * qSin(v: radian);
195
196 d->m_largeArc = {xShift + pointX, yShift - pointY};
197
198 shapePath->setPath(m_painterPath);
199 m_painterPath.clear();
200
201 radian = qDegreesToRadians(degrees: slice->startAngle() + (slice->angleSpan() * .5));
202 startBigX = radius * qSin(v: radian);
203 startBigY = radius * qCos(v: radian);
204
205 pointX = radius * (1.0 + d->m_labelArmLengthFactor) * qSin(v: radian);
206 pointY = radius * (1.0 + d->m_labelArmLengthFactor) * qCos(v: radian);
207
208 m_painterPath.moveTo(x: xShift + startBigX, y: yShift - startBigY);
209 m_painterPath.lineTo(x: xShift + pointX, y: yShift - pointY);
210
211 d->m_centerLine = {xShift + pointX, yShift - pointY};
212
213 d->m_labelArm = {xShift + pointX, yShift - pointY};
214
215 auto labelWidth = radian > M_PI ? -d->m_labelItem->width() : d->m_labelItem->width();
216 m_painterPath.lineTo(x: d->m_labelArm.x() + labelWidth, y: d->m_labelArm.y());
217
218 d->setLabelPosition(d->m_labelPosition);
219 d->m_labelPath->setPath(m_painterPath);
220
221 sliceAngle += slice->angleSpan();
222 sliceIndex++;
223 legendDataList.push_back(t: {.color: color, .borderColor: borderColor, .label: d->m_labelText});
224 }
225
226 series->d_func()->setLegendData(legendDataList);
227}
228
229void PieRenderer::afterPolish(QList<QAbstractSeries *> &cleanupSeries)
230{
231 for (auto series : cleanupSeries) {
232 auto pieSeries = qobject_cast<QPieSeries *>(object: series);
233 if (pieSeries) {
234 auto slices = pieSeries->slices();
235 for (QPieSlice *slice : std::as_const(t&: slices)) {
236 QPieSlicePrivate *d = slice->d_func();
237 auto labelElements = d->m_labelPath->pathElements();
238 auto shapeElements = d->m_shapePath->pathElements();
239
240 labelElements.clear(&labelElements);
241 shapeElements.clear(&shapeElements);
242
243 slice->deleteLater();
244 d->m_labelItem->deleteLater();
245
246 m_activeSlices.remove(key: slice);
247 }
248 }
249 }
250}
251
252void PieRenderer::updateSeries(QPieSeries *series)
253{
254 auto needPolish = false;
255
256 for (auto &sliceData : m_activeSlices) {
257 if (!sliceData.initialized) {
258 sliceData.initialized = true;
259 needPolish = true;
260 }
261 }
262
263 if (needPolish)
264 handlePolish(series);
265}
266
267void PieRenderer::afterUpdate(QList<QAbstractSeries *> &cleanupSeries)
268{
269 Q_UNUSED(cleanupSeries);
270}
271
272void PieRenderer::markedDeleted(QList<QPieSlice *> deleted)
273{
274 auto emptyPath = QPainterPath{};
275
276 for (auto slice : deleted) {
277 auto d = slice->d_func();
278 d->m_shapePath->setPath(emptyPath);
279 d->m_labelPath->setPath(emptyPath);
280 d->m_labelItem->deleteLater();
281 m_activeSlices.remove(key: slice);
282 }
283}
284
285bool PieRenderer::isPointInSlice(QPointF point, QPieSlice *slice, qreal *angle)
286{
287 QPieSeries* series = slice->series();
288 QPointF center = QPointF(size().width() * series->horizontalPosition(),
289 size().height() * series->verticalPosition());
290 qreal radius = size().width() > size().height() ? size().height() : size().width();
291 radius *= (.5 * series->pieSize());
292
293 qreal explodeDistance = .0;
294 if (slice->isExploded())
295 explodeDistance = slice->explodeDistanceFactor() * radius;
296 qreal radian = qDegreesToRadians(degrees: slice->startAngle() + (slice->angleSpan() * .5));
297 qreal xShift = center.x() + (explodeDistance * qSin(v: radian));
298 qreal yShift = center.y() - (explodeDistance * qCos(v: radian));
299
300 QPointF adjustedPosition = QPointF(point.x() - xShift,
301 point.y() - yShift);
302 qreal distance = qSqrt(v: qPow(x: adjustedPosition.x(), y: 2) +
303 qPow(x: adjustedPosition.y(), y: 2));
304 qreal foundAngle = qRadiansToDegrees(radians: qAtan2(y: adjustedPosition.y(), x: adjustedPosition.x())) + 90;
305 if (foundAngle < 0)
306 foundAngle += 360;
307
308 if (angle)
309 *angle = foundAngle;
310
311 if (distance <= radius
312 && foundAngle >= slice->startAngle()
313 && foundAngle <= (slice->startAngle() + slice->angleSpan())) {
314 return true;
315 }
316 return false;
317}
318
319bool PieRenderer::handleHoverMove(QHoverEvent *event)
320{
321 bool handled = false;
322 const QPointF &position = event->position();
323
324 bool hovering = false;
325 QList<QPieSlice *> list = m_activeSlices.keys();
326 for (const auto &slice : std::as_const(t&: list)) {
327 if (!slice->series()->isHoverable())
328 continue;
329
330 qreal angle;
331 if (isPointInSlice(point: position, slice, angle: &angle)) {
332 const QString &name = slice->series()->name();
333 const QPointF value(slice->startAngle(), angle);
334
335 if (!m_currentHoverSlice) {
336 m_currentHoverSlice = slice;
337 slice->series()->setHovered(true);
338 emit slice->series()->hoverEnter(seriesName: name, position, value);
339 }
340 if (m_currentHoverSlice != slice) {
341 slice->series()->setHovered(true);
342 emit m_currentHoverSlice->series()->hoverExit(seriesName: name, position);
343 emit slice->series()->hoverEnter(seriesName: name, position, value);
344 m_currentHoverSlice = slice;
345 }
346
347 emit slice->series()->hover(seriesName: name, position, value);
348 hovering = true;
349 handled = true;
350 }
351 }
352
353 if (!hovering && m_currentHoverSlice) {
354 m_currentHoverSlice->series()->setHovered(false);
355 emit m_currentHoverSlice->series()->
356 hoverExit(seriesName: m_currentHoverSlice->series()->name(), position);
357 m_currentHoverSlice = nullptr;
358 handled = true;
359 }
360 return handled;
361}
362
363void PieRenderer::onSingleTapped(QEventPoint eventPoint, Qt::MouseButton button)
364{
365 Q_UNUSED(button)
366
367 QList<QPieSlice *> list = m_activeSlices.keys();
368 for (const auto &pieSlice : std::as_const(t&: list)) {
369 if (!pieSlice->series()->isSelectable())
370 continue;
371
372 if (isPointInSlice(point: eventPoint.position(), slice: pieSlice)) {
373 emit pieSlice->series()->clicked(slice: pieSlice);
374 return;
375 }
376 }
377}
378
379void PieRenderer::onDoubleTapped(QEventPoint eventPoint, Qt::MouseButton button)
380{
381 Q_UNUSED(button)
382
383 QList<QPieSlice *> list = m_activeSlices.keys();
384 for (const auto &pieSlice : std::as_const(t&: list)) {
385 if (!pieSlice->series()->isSelectable())
386 continue;
387
388 if (isPointInSlice(point: eventPoint.position(), slice: pieSlice)) {
389 emit pieSlice->series()->doubleClicked(slice: pieSlice);
390 return;
391 }
392 }
393}
394
395void PieRenderer::onPressedChanged()
396{
397 QList<QPieSlice *> list = m_activeSlices.keys();
398 for (const auto &pieSlice : std::as_const(t&: list)) {
399 if (!pieSlice->series()->isSelectable())
400 continue;
401
402 if (isPointInSlice(point: m_tapHandler->point().position(), slice: pieSlice)) {
403 if (m_tapHandler->isPressed())
404 emit pieSlice->series()->pressed(slice: pieSlice);
405 else
406 emit pieSlice->series()->released(slice: pieSlice);
407 return;
408 }
409 }
410}
411
412QT_END_NAMESPACE
413

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