1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <private/piesliceitem_p.h>
5#include <private/piechartitem_p.h>
6#include <QtCharts/QPieSeries>
7#include <QtCharts/QPieSlice>
8#include <private/chartpresenter_p.h>
9#include <QtGui/QPainter>
10#include <QtCore/QtMath>
11#include <QtWidgets/QGraphicsSceneEvent>
12#include <QtCore/QTime>
13#include <QtGui/QTextDocument>
14#include <QtCore/QDebug>
15
16QT_BEGIN_NAMESPACE
17
18QPointF offset(qreal angle, qreal length)
19{
20 qreal dx = qSin(v: qDegreesToRadians(degrees: angle)) * length;
21 qreal dy = qCos(v: qDegreesToRadians(degrees: angle)) * length;
22 return QPointF(dx, -dy);
23}
24
25PieSliceItem::PieSliceItem(QGraphicsItem *parent)
26 : QGraphicsObject(parent),
27 m_hovered(false),
28 m_mousePressed(false)
29{
30 setAcceptHoverEvents(true);
31 setAcceptedMouseButtons(Qt::MouseButtonMask);
32 setZValue(ChartPresenter::PieSeriesZValue);
33 setFlag(flag: QGraphicsItem::ItemIsSelectable);
34 m_labelItem = new QGraphicsTextItem(this);
35 m_labelItem->document()->setDocumentMargin(1.0);
36}
37
38PieSliceItem::~PieSliceItem()
39{
40 // If user is hovering over the slice and it gets destroyed we do
41 // not get a hover leave event. So we must emit the signal here.
42 if (m_hovered)
43 emit hovered(state: false);
44}
45
46QRectF PieSliceItem::boundingRect() const
47{
48 return m_boundingRect;
49}
50
51QPainterPath PieSliceItem::shape() const
52{
53 // Don't include the label and label arm.
54 // This is used to detect a mouse clicks. We do not want clicks from label.
55 return m_slicePath;
56}
57
58void PieSliceItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
59{
60 painter->save();
61 painter->setClipRect(parentItem()->boundingRect());
62 painter->setPen(m_data.m_slicePen);
63 painter->setBrush(m_data.m_sliceBrush);
64 painter->drawPath(path: m_slicePath);
65 painter->restore();
66
67 if (m_data.m_isLabelVisible) {
68 painter->save();
69
70 // Pen for label arm not defined in the QPieSeries api, let's use brush's color instead
71 painter->setBrush(m_data.m_labelBrush);
72
73 if (m_data.m_labelPosition == QPieSlice::LabelOutside) {
74 painter->setClipRect(parentItem()->boundingRect());
75 painter->strokePath(path: m_labelArmPath, pen: m_data.m_labelBrush.color());
76 }
77
78 painter->restore();
79 }
80}
81
82void PieSliceItem::hoverEnterEvent(QGraphicsSceneHoverEvent * /*event*/)
83{
84 m_hovered = true;
85 emit hovered(state: true);
86}
87
88void PieSliceItem::hoverLeaveEvent(QGraphicsSceneHoverEvent * /*event*/)
89{
90 m_hovered = false;
91 emit hovered(state: false);
92}
93
94void PieSliceItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
95{
96 emit pressed(buttons: event->buttons());
97 m_mousePressed = true;
98}
99
100void PieSliceItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
101{
102 emit released(buttons: event->buttons());
103 if (m_mousePressed)
104 emit clicked(buttons: event->buttons());
105}
106
107void PieSliceItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
108{
109 // For Pie slice a press signal needs to be explicitly fired for mouseDoubleClickEvent
110 emit pressed(buttons: event->buttons());
111 emit doubleClicked(buttons: event->buttons());
112}
113
114void PieSliceItem::setLayout(const PieSliceData &sliceData)
115{
116 m_data = sliceData;
117 updateGeometry();
118 update();
119}
120
121void PieSliceItem::updateGeometry()
122{
123 if (m_data.m_radius <= 0)
124 return;
125
126 prepareGeometryChange();
127
128 // slice path
129 qreal centerAngle;
130 QPointF armStart;
131 m_slicePath = slicePath(center: m_data.m_center, radius: m_data.m_radius, startAngle: m_data.m_startAngle, angleSpan: m_data.m_angleSpan, centerAngle: &centerAngle, armStart: &armStart);
132
133 m_labelItem->setVisible(m_data.m_isLabelVisible);
134
135 if (m_data.m_isLabelVisible) {
136 // text rect
137 m_labelTextRect = ChartPresenter::textBoundingRect(font: m_data.m_labelFont,
138 text: m_data.m_labelText,
139 angle: 0);
140
141 QString label(m_data.m_labelText);
142 m_labelItem->setDefaultTextColor(m_data.m_labelBrush.color());
143 m_labelItem->setFont(m_data.m_labelFont);
144
145 // text position
146 if (m_data.m_labelPosition == QPieSlice::LabelOutside) {
147 setFlag(flag: QGraphicsItem::ItemClipsChildrenToShape, enabled: false);
148
149 // label arm path
150 QPointF labelTextStart;
151 m_labelArmPath = labelArmPath(start: armStart, angle: centerAngle,
152 length: m_data.m_radius * m_data.m_labelArmLengthFactor,
153 textWidth: m_labelTextRect.width(), textStart: &labelTextStart);
154
155 m_labelTextRect.moveBottomLeft(p: labelTextStart);
156 if (m_labelTextRect.left() < 0)
157 m_labelTextRect.setLeft(0);
158 else if (m_labelTextRect.left() < parentItem()->boundingRect().left())
159 m_labelTextRect.setLeft(parentItem()->boundingRect().left());
160 if (m_labelTextRect.right() > parentItem()->boundingRect().right())
161 m_labelTextRect.setRight(parentItem()->boundingRect().right());
162
163 label = ChartPresenter::truncatedText(font: m_data.m_labelFont, text: m_data.m_labelText,
164 angle: qreal(0.0), maxWidth: m_labelTextRect.width(),
165 maxHeight: m_labelTextRect.height(), boundingRect&: m_labelTextRect);
166 m_labelArmPath = labelArmPath(start: armStart, angle: centerAngle,
167 length: m_data.m_radius * m_data.m_labelArmLengthFactor,
168 textWidth: m_labelTextRect.width(), textStart: &labelTextStart);
169 m_labelTextRect.moveBottomLeft(p: labelTextStart);
170
171 m_labelItem->setTextWidth(m_labelTextRect.width()
172 + m_labelItem->document()->documentMargin());
173 m_labelItem->setHtml(label);
174 m_labelItem->setRotation(0);
175 m_labelItem->setPos(ax: m_labelTextRect.x(), ay: m_labelTextRect.y() + 1.0);
176 } else {
177 // label inside
178 setFlag(flag: QGraphicsItem::ItemClipsChildrenToShape);
179 m_labelItem->setTextWidth(m_labelTextRect.width()
180 + m_labelItem->document()->documentMargin());
181 m_labelItem->setHtml(label);
182
183 QPointF textCenter;
184 if (m_data.m_holeRadius > 0) {
185 textCenter = m_data.m_center + offset(angle: centerAngle, length: m_data.m_holeRadius
186 + (m_data.m_radius
187 - m_data.m_holeRadius) / 2);
188 } else {
189 textCenter = m_data.m_center + offset(angle: centerAngle, length: m_data.m_radius / 2);
190 }
191 m_labelItem->setPos(ax: textCenter.x() - m_labelItem->boundingRect().width() / 2,
192 ay: textCenter.y() - m_labelTextRect.height() / 2);
193
194 QPointF labelCenter = m_labelItem->boundingRect().center();
195 m_labelItem->setTransformOriginPoint(labelCenter);
196
197 if (m_data.m_labelPosition == QPieSlice::LabelInsideTangential) {
198 m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2);
199 } else if (m_data.m_labelPosition == QPieSlice::LabelInsideNormal) {
200 if (m_data.m_startAngle + m_data.m_angleSpan / 2 < 180)
201 m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2 - 90);
202 else
203 m_labelItem->setRotation(m_data.m_startAngle + m_data.m_angleSpan / 2 + 90);
204 } else {
205 m_labelItem->setRotation(0);
206 }
207 }
208 // Hide label if it's outside the bounding rect of parent item
209 QRectF labelRect(m_labelItem->boundingRect());
210 labelRect.moveTopLeft(p: m_labelItem->pos());
211 if ((parentItem()->boundingRect().left()
212 < (labelRect.left() + m_labelItem->document()->documentMargin() + 1.0))
213 && (parentItem()->boundingRect().right()
214 > (labelRect.right() - m_labelItem->document()->documentMargin() - 1.0))
215 && (parentItem()->boundingRect().top()
216 < (labelRect.top() + m_labelItem->document()->documentMargin() + 1.0))
217 && (parentItem()->boundingRect().bottom()
218 > (labelRect.bottom() - m_labelItem->document()->documentMargin() - 1.0)))
219 m_labelItem->show();
220 else
221 m_labelItem->hide();
222 }
223
224 // bounding rect
225 if (m_data.m_isLabelVisible)
226 m_boundingRect = m_slicePath.boundingRect().united(r: m_labelArmPath.boundingRect()).united(r: m_labelTextRect);
227 else
228 m_boundingRect = m_slicePath.boundingRect();
229
230 // Inflate bounding rect by 2/3 pen width to make sure it encompasses whole slice also for thick pens
231 // and miter joins.
232 int penWidth = (m_data.m_slicePen.width() * 2) / 3;
233 m_boundingRect = m_boundingRect.adjusted(xp1: -penWidth, yp1: -penWidth, xp2: penWidth, yp2: penWidth);
234}
235
236QPointF PieSliceItem::sliceCenter(QPointF point, qreal radius, QPieSlice *slice)
237{
238 if (slice->isExploded()) {
239 qreal centerAngle = slice->startAngle() + (slice->angleSpan() / 2);
240 qreal len = radius * slice->explodeDistanceFactor();
241 point += offset(angle: centerAngle, length: len);
242 }
243 return point;
244}
245
246QPainterPath PieSliceItem::slicePath(QPointF center, qreal radius, qreal startAngle, qreal angleSpan, qreal *centerAngle, QPointF *armStart)
247{
248 // calculate center angle
249 *centerAngle = startAngle + (angleSpan / 2);
250
251 // calculate slice rectangle
252 QRectF rect(center.x() - radius, center.y() - radius, radius * 2, radius * 2);
253
254 // slice path
255 QPainterPath path;
256 if (m_data.m_holeRadius > 0) {
257 QRectF insideRect(center.x() - m_data.m_holeRadius, center.y() - m_data.m_holeRadius, m_data.m_holeRadius * 2, m_data.m_holeRadius * 2);
258 path.arcMoveTo(rect, angle: -startAngle + 90);
259 path.arcTo(rect, startAngle: -startAngle + 90, arcLength: -angleSpan);
260 path.arcTo(rect: insideRect, startAngle: -startAngle + 90 - angleSpan, arcLength: angleSpan);
261 path.closeSubpath();
262 } else {
263 path.moveTo(p: rect.center());
264 path.arcTo(rect, startAngle: -startAngle + 90, arcLength: -angleSpan);
265 path.closeSubpath();
266 }
267
268 // calculate label arm start point
269 *armStart = center;
270 *armStart += offset(angle: *centerAngle, length: radius + PIESLICE_LABEL_GAP);
271
272 return path;
273}
274
275QPainterPath PieSliceItem::labelArmPath(QPointF start, qreal angle, qreal length, qreal textWidth, QPointF *textStart)
276{
277 // Normalize the angle to 0-360 range
278 // NOTE: We are using int here on purpose. Depenging on platform and hardware
279 // qreal can be a double, float or something the user gives to the Qt configure
280 // (QT_COORD_TYPE). Compilers do not seem to support modulo for double or float
281 // but there are fmod() and fmodf() functions for that. So instead of some #ifdef
282 // that might break we just use int. Precision for this is just fine for our needs.
283 int normalized = angle * 10.0;
284 normalized = normalized % 3600;
285 if (normalized < 0)
286 normalized += 3600;
287 angle = (qreal) normalized / 10.0;
288
289 // prevent label arm pointing straight down because it will look bad
290 if (angle < 180 && angle > 170)
291 angle = 170;
292 if (angle > 180 && angle < 190)
293 angle = 190;
294
295 // line from slice to label
296 QPointF parm1 = start + offset(angle, length);
297
298 // line to underline the label
299 QPointF parm2 = parm1;
300 if (angle < 180) { // arm swings the other way on the left side
301 parm2 += QPointF(textWidth, 0);
302 *textStart = parm1;
303 } else {
304 parm2 += QPointF(-textWidth, 0);
305 *textStart = parm2;
306 }
307
308 QPainterPath path;
309 path.moveTo(p: start);
310 path.lineTo(p: parm1);
311 path.lineTo(p: parm2);
312
313 return path;
314}
315
316QT_END_NAMESPACE
317
318#include "moc_piesliceitem_p.cpp"
319

source code of qtcharts/src/charts/piechart/piesliceitem.cpp