KD Chart API Documentation 3.1
Loading...
Searching...
No Matches
KDChartPieDiagram.cpp
Go to the documentation of this file.
1/****************************************************************************
2**
3** This file is part of the KD Chart library.
4**
5** SPDX-FileCopyrightText: 2001 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
6**
7** SPDX-License-Identifier: MIT
8**
9****************************************************************************/
10
11#include <QDebug>
12#include <QPainter>
13#include <QStack>
14
15#include "KDChartPieDiagram.h"
16#include "KDChartPieDiagram_p.h"
17
18#include "KDChartPaintContext.h"
19#include "KDChartPainterSaver_p.h"
21#include "KDChartPolarCoordinatePlane_p.h"
23
24#include <KDABLibFakes>
25
26using namespace KDChart;
27
28PieDiagram::Private::Private()
29 : labelDecorations(PieDiagram::NoDecoration)
30{
31}
32
33PieDiagram::Private::~Private()
34{
35}
36
37#define d d_func()
38
40 : AbstractPieDiagram(new Private(), parent, plane)
41{
42 init();
43}
44
48
49void PieDiagram::init()
50{
51}
52
57{
58 return new PieDiagram(new Private(*d));
59}
60
61void PieDiagram::setLabelDecorations(LabelDecorations decorations)
62{
63 d->labelDecorations = decorations;
64}
65
67{
68 return d->labelDecorations;
69}
70
72{
73 d->isCollisionAvoidanceEnabled = enabled;
74}
75
77{
78 return d->isCollisionAvoidanceEnabled;
79}
80
82{
83 if (!checkInvariants(true) || model()->rowCount() < 1)
84 return QPair<QPointF, QPointF>(QPointF(0, 0), QPointF(0, 0));
85
87
88 QPointF bottomLeft(QPointF(0, 0));
89 QPointF topRight;
90 // If we explode, we need extra space for the slice that has the largest explosion distance.
91 if (attrs.explode()) {
92 const int colCount = columnCount();
93 qreal maxExplode = 0.0;
94 for (int j = 0; j < colCount; ++j) {
95 const PieAttributes columnAttrs(pieAttributes(model()->index(0, j, rootIndex()))); // checked
96 maxExplode = qMax(maxExplode, columnAttrs.explodeFactor());
97 }
98 topRight = QPointF(1.0 + maxExplode, 1.0 + maxExplode);
99 } else {
100 topRight = QPointF(1.0, 1.0);
101 }
102 return QPair<QPointF, QPointF>(bottomLeft, topRight);
103}
104
106{
107 QPainter painter(viewport());
109 ctx.setPainter(&painter);
110 ctx.setRectangle(QRectF(0, 0, width(), height()));
111 paint(&ctx);
112}
113
117
119{
120}
121
123{
124 // Painting is a two stage process
125 // In the first stage we figure out how much space is needed
126 // for text labels.
127 // In the second stage, we make use of that information and
128 // perform the actual painting.
129 placeLabels(ctx);
130 paintInternal(ctx);
131}
132
133void PieDiagram::calcSliceAngles()
134{
135 // determine slice positions and sizes
136 const qreal sum = valueTotals();
137 const qreal sectorsPerValue = 360.0 / sum;
139 qreal currentValue = plane ? plane->startPosition() : 0.0;
140
141 const int colCount = columnCount();
142 d->startAngles.resize(colCount);
143 d->angleLens.resize(colCount);
144
145 bool atLeastOneValue = false; // guard against completely empty tables
146 for (int iColumn = 0; iColumn < colCount; ++iColumn) {
147 bool isOk;
148 const qreal cellValue = qAbs(model()->data(model()->index(0, iColumn, rootIndex())) // checked
149 .toReal(&isOk));
150 // toReal() returns 0.0 if there was no value or a non-numeric value
152
153 d->startAngles[iColumn] = currentValue;
154 d->angleLens[iColumn] = cellValue * sectorsPerValue;
155
156 currentValue = d->startAngles[iColumn] + d->angleLens[iColumn];
157 }
158
159 // If there was no value at all, this is the sign for other code to bail out
160 if (!atLeastOneValue) {
161 d->startAngles.clear();
162 d->angleLens.clear();
163 }
164}
165
166void PieDiagram::calcPieSize(const QRectF &contentsRect)
167{
169
170 // if any slice explodes, the whole pie needs additional space so we make the basic size smaller
171 qreal maxExplode = 0.0;
172 const int colCount = columnCount();
173 for (int j = 0; j < colCount; ++j) {
174 const PieAttributes columnAttrs(pieAttributes(model()->index(0, j, rootIndex()))); // checked
175 maxExplode = qMax(maxExplode, columnAttrs.explodeFactor());
176 }
177 d->size /= (1.0 + 1.0 * maxExplode);
178
179 if (d->size < 0.0) {
180 d->size = 0;
181 }
182}
183
184// this is the rect of the top surface of the pie, i.e. excluding the "3D" rim effect.
185QRectF PieDiagram::twoDPieRect(const QRectF &contentsRect, const ThreeDPieAttributes &threeDAttrs) const
186{
188 if (!threeDAttrs.isEnabled()) {
189 qreal x = (contentsRect.width() - d->size) / 2.0;
190 qreal y = (contentsRect.height() - d->size) / 2.0;
191 pieRect = QRectF(contentsRect.left() + x, contentsRect.top() + y, d->size, d->size);
192 } else {
193 // threeD: width is the maximum possible width; height is 1/2 of that
195
196 qreal x = (contentsRect.width() - d->size) / 2.0;
197 qreal height = d->size;
198 // make sure that the height plus the threeDheight is not more than the
199 // available size
200 if (threeDAttrs.depth() >= 0.0) {
201 // positive pie height: absolute value
203 height = d->size - sizeFor3DEffect;
204 } else {
205 // negative pie height: relative value
206 sizeFor3DEffect = -threeDAttrs.depth() / 100.0 * height;
207 height = d->size - sizeFor3DEffect;
208 }
210
212 }
213 return pieRect;
214}
215
216void PieDiagram::placeLabels(PaintContext *paintContext)
217{
218 if (!checkInvariants(true) || model()->rowCount() < 1) {
219 return;
220 }
221 if (paintContext->rectangle().isEmpty() || valueTotals() == 0.0) {
222 return;
223 }
224
226 const int colCount = columnCount();
227
228 d->reverseMapper.clear(); // on first call, this sets up the internals of the ReverseMapper.
229
230 calcSliceAngles();
231 if (d->startAngles.isEmpty()) {
232 return;
233 }
234
235 calcPieSize(paintContext->rectangle());
236
237 // keep resizing the pie until the labels and the pie fit into paintContext->rectangle()
238
239 bool tryAgain = true;
240 while (tryAgain) {
241 tryAgain = false;
242
243 QRectF pieRect = twoDPieRect(paintContext->rectangle(), threeDAttrs);
244 d->forgetAlreadyPaintedDataValues();
245 d->labelPaintCache.clear();
246
247 for (int slice = 0; slice < colCount; slice++) {
248 if (d->angleLens[slice] != 0.0) {
249 const QRectF explodedPieRect = explodedDrawPosition(pieRect, slice);
250 addSliceLabel(&d->labelPaintCache, explodedPieRect, slice);
251 }
252 }
253
255 d->paintDataValueTextsAndMarkers(paintContext, d->labelPaintCache, false, true,
257 if (d->isCollisionAvoidanceEnabled) {
258 shuffleLabels(&textBoundingRect);
259 }
260
261 if (!textBoundingRect.isEmpty() && d->size > 0.0) {
262 const QRectF &clipRect = paintContext->rectangle();
263 // see by how many pixels the text is clipped on each side
264 qreal right = qMax(qreal(0.0), textBoundingRect.right() - clipRect.right());
265 qreal left = qMax(qreal(0.0), clipRect.left() - textBoundingRect.left());
266 // attention here - y coordinates in Qt are inverted compared to the convention in maths
267 qreal top = qMax(qreal(0.0), clipRect.top() - textBoundingRect.top());
268 qreal bottom = qMax(qreal(0.0), textBoundingRect.bottom() - clipRect.bottom());
269 qreal maxOverhang = qMax(qMax(right, left), qMax(top, bottom));
270
271 if (maxOverhang > 0.0) {
272 // subtract 2x as much because every side only gets half of the total diameter reduction
273 // and we have to make up for the overhang on one particular side.
274 d->size -= qMin(d->size, maxOverhang * ( qreal )2.0);
275 tryAgain = true;
276 }
277 }
278 }
279}
280
281static int wraparound(int i, int size)
282{
283 while (i < 0) {
284 i += size;
285 }
286 while (i >= size) {
287 i -= size;
288 }
289 return i;
290}
291
292// #define SHUFFLE_DEBUG
293
294void PieDiagram::shuffleLabels(QRectF *textBoundingRect)
295{
296 // things that could be improved here:
297 // - use a variable number (chosen using angle information) of neighbors to check
298 // - try harder to arrange the labels to look nice
299
300 // ideas:
301 // - leave labels that don't collide alone (only if they their offset is zero)
302 // - use a graphics view for collision detection
303
304 LabelPaintCache &lpc = d->labelPaintCache;
305 const int n = lpc.paintReplay.size();
306 bool modified = false;
307 qreal direction = 5.0;
309 offsets.fill(0.0, n);
310
311 for (bool lastRoundModified = true; lastRoundModified;) {
312 lastRoundModified = false;
313
314 for (int i = 0; i < n; i++) {
315 const int neighborsToCheck = qMax(10, lpc.paintReplay.size() - 1);
316 const int minComp = wraparound(i - neighborsToCheck / 2, n);
317 const int maxComp = wraparound(i + (neighborsToCheck + 1) / 2, n);
318
319 QPainterPath &path = lpc.paintReplay[i].labelArea;
320
321 for (int j = minComp; j != maxComp; j = wraparound(j + 1, n)) {
322 if (i == j) {
323 continue;
324 }
325 QPainterPath &otherPath = lpc.paintReplay[j].labelArea;
326
327 while ((offsets[i] + direction > 0) && otherPath.intersects(path)) {
328#ifdef SHUFFLE_DEBUG
329 qDebug() << "collision involving" << j << "and" << i << " -- n =" << n;
330 TextAttributes ta = lpc.paintReplay[i].attrs.textAttributes();
332 lpc.paintReplay[i].attrs.setTextAttributes(ta);
333#endif
334 uint slice = lpc.paintReplay[i].index.column();
335 qreal angle = DEGTORAD(d->startAngles[slice] + d->angleLens[slice] / 2.0);
336 qreal dx = cos(angle) * direction;
337 qreal dy = -sin(angle) * direction;
338 offsets[i] += direction;
339 path.translate(dx, dy);
340 lastRoundModified = true;
341 }
342 }
343 }
344 direction *= -1.07; // this can "overshoot", but avoids getting trapped in local minimums
345 modified = modified || lastRoundModified;
346 }
347
348 if (modified) {
349 for (int i = 0; i < lpc.paintReplay.size(); i++) {
350 *textBoundingRect |= lpc.paintReplay[i].labelArea.boundingRect();
351 }
352 }
353}
354
356{
357 QPolygonF ret;
358 for (int i = 0; i < pp.elementCount(); i++) {
359 const QPainterPath::Element &el = pp.elementAt(i);
360 Q_ASSERT(el.type == QPainterPath::MoveToElement || el.type == QPainterPath::LineToElement);
361 ret.append(el);
362 }
363 return ret;
364}
365
366// you can call it "normalizedProjectionLength" if you like
367static qreal normProjection(const QLineF &l1, const QLineF &l2)
368{
369 const qreal dotProduct = l1.dx() * l2.dx() + l1.dy() * l2.dy();
370 return qAbs(dotProduct / (l1.length() * l2.length()));
371}
372
373static QLineF labelAttachmentLine(const QPointF &center, const QPointF &start, const QPainterPath &label)
374{
375 Q_ASSERT(label.elementCount() == 5);
376
377 // start is assumed to lie on the outer rim of the slice(!), making it possible to derive the
378 // radius of the pie
379 const qreal pieRadius = QLineF(center, start).length();
380
381 // don't draw a line at all when the label is connected to its slice due to at least one of its
382 // corners falling inside the slice.
383 for (int i = 0; i < 4; i++) { // point 4 is just a duplicate of point 0
384 if (QLineF(label.elementAt(i), center).length() < pieRadius) {
385 return QLineF();
386 }
387 }
388
389 // find the closest edge in the polygon, and its two neighbors
390 QPointF closeCorners[3];
391 {
392 QPointF closest = QPointF(1000000, 1000000);
393 int closestIndex = 0; // better misbehave than crash
394 for (int i = 0; i < 4; i++) { // point 4 is just a duplicate of point 0
395 QPointF p = label.elementAt(i);
396 if (QLineF(p, center).length() < QLineF(closest, center).length()) {
397 closest = p;
398 closestIndex = i;
399 }
400 }
401
402 closeCorners[0] = label.elementAt(wraparound(closestIndex - 1, 4));
403 closeCorners[1] = closest;
404 closeCorners[2] = label.elementAt(wraparound(closestIndex + 1, 4));
405 }
406
407 QLineF edge1 = QLineF(closeCorners[0], closeCorners[1]);
408 QLineF edge2 = QLineF(closeCorners[1], closeCorners[2]);
409 QLineF connection1 = QLineF((closeCorners[0] + closeCorners[1]) / 2.0, center);
410 QLineF connection2 = QLineF((closeCorners[1] + closeCorners[2]) / 2.0, center);
411 QLineF ret;
412 // prefer the connecting line meeting its edge at a more perpendicular angle
413 if (normProjection(edge1, connection1) < normProjection(edge2, connection2)) {
414 ret = connection1;
415 } else {
416 ret = connection2;
417 }
418
419 // This tends to look a bit better than not doing it *shrug*
420 ret.setP2((start + center) / 2.0);
421
422 // make the line end at the rim of the slice (not 100% accurate because the line is not precisely radial)
423 qreal p1Radius = QLineF(ret.p1(), center).length();
424 ret.setLength(p1Radius - pieRadius);
425
426 return ret;
427}
428
429void PieDiagram::paintInternal(PaintContext *paintContext)
430{
431 // note: Not having any data model assigned is no bug
432 // but we can not draw a diagram then either.
433 if (!checkInvariants(true) || model()->rowCount() < 1) {
434 return;
435 }
436 if (d->startAngles.isEmpty() || paintContext->rectangle().isEmpty() || valueTotals() == 0.0) {
437 return;
438 }
439
441 const int colCount = columnCount();
442
443 // Paint from back to front ("painter's algorithm") - first draw the backmost slice,
444 // then the slices on the left and right from back to front, then the frontmost one.
445
446 QRectF pieRect = twoDPieRect(paintContext->rectangle(), threeDAttrs);
447 const int backmostSlice = findSliceAt(90, colCount);
448 const int frontmostSlice = findSliceAt(270, colCount);
451
452 drawSlice(paintContext->painter(), pieRect, backmostSlice);
453
455 const int rightmostSlice = findSliceAt(0, colCount);
456 const int leftmostSlice = findSliceAt(180, colCount);
457
460 }
463 }
464 }
465
468 drawSlice(paintContext->painter(), pieRect, currentLeftSlice);
469 }
471 }
472
475 drawSlice(paintContext->painter(), pieRect, currentRightSlice);
476 }
478 }
479
480 // if the backmost slice is not the frontmost slice, we draw the frontmost one last
482 drawSlice(paintContext->painter(), pieRect, frontmostSlice);
483 }
484
485 d->paintDataValueTextsAndMarkers(paintContext, d->labelPaintCache, false, false);
486 // it's safer to do this at the beginning of placeLabels, but we can save some memory here.
487 d->forgetAlreadyPaintedDataValues();
488 // ### maybe move this into AbstractDiagram, also make ReverseMapper deal better with multiple polygons
489 const QPointF center = paintContext->rectangle().center();
490 const PainterSaver painterSaver(paintContext->painter());
491 paintContext->painter()->setBrush(Qt::NoBrush);
493 // we expect the PainterPath to be a rectangle
494 if (pi.labelArea.elementCount() != 5) {
495 continue;
496 }
497
498 paintContext->painter()->setPen(pen(pi.index));
499 if (d->labelDecorations & LineFromSliceDecoration) {
500 paintContext->painter()->drawLine(labelAttachmentLine(center, pi.markerPos, pi.labelArea));
501 }
502 if (d->labelDecorations & FrameDecoration) {
503 paintContext->painter()->drawPath(pi.labelArea);
504 }
505 d->reverseMapper.addPolygon(pi.index.row(), pi.index.column(),
506 polygonFromPainterPath(pi.labelArea));
507 }
508 d->labelPaintCache.clear();
509 d->startAngles.clear();
510 d->angleLens.clear();
511}
512
513#if defined(Q_OS_WIN)
514#define trunc(x) (( int )(x))
515#endif
516
517QRectF PieDiagram::explodedDrawPosition(const QRectF &drawPosition, uint slice) const
518{
519 const QModelIndex index(model()->index(0, slice, rootIndex())); // checked
520 const PieAttributes attrs(pieAttributes(index));
521
523 if (attrs.explode()) {
524 qreal startAngle = d->startAngles[slice];
525 qreal angleLen = d->angleLens[slice];
526 qreal explodeAngle = (DEGTORAD(startAngle + angleLen / 2.0));
527 qreal explodeDistance = attrs.explodeFactor() * d->size / 2.0;
528
531 }
533}
534
543void PieDiagram::drawSlice(QPainter *painter, const QRectF &drawPosition, uint slice)
544{
545 // Is there anything to draw at all?
546 if (d->angleLens[slice] == 0.0) {
547 return;
548 }
549 const QRectF adjustedDrawPosition = explodedDrawPosition(drawPosition, slice);
550 draw3DEffect(painter, adjustedDrawPosition, slice);
551 drawSliceSurface(painter, adjustedDrawPosition, slice);
552}
553
561void PieDiagram::drawSliceSurface(QPainter *painter, const QRectF &drawPosition, uint slice)
562{
563 // Is there anything to draw at all?
564 const qreal angleLen = d->angleLens[slice];
565 const qreal startAngle = d->startAngles[slice];
566 const QModelIndex index(model()->index(0, slice, rootIndex())); // checked
567
568 const PieAttributes attrs(pieAttributes(index));
570
572 QBrush br = brush(index);
573 if (threeDAttrs.isEnabled()) {
574 br = threeDAttrs.threeDBrush(br, drawPosition);
575 }
576 painter->setBrush(br);
577
578 QPen pen = this->pen(index);
579 if (threeDAttrs.isEnabled()) {
581 }
582 painter->setPen(pen);
583
584 if (angleLen == 360) {
585 // full circle, avoid nasty line in the middle
586 painter->drawEllipse(drawPosition);
587
588 // Add polygon to Reverse mapper for showing tool tips.
590 d->reverseMapper.addPolygon(index.row(), index.column(), poly);
591 } else {
592 // draw the top of this piece
593 // Start with getting the points for the arc.
594 const int arcPoints = static_cast<int>(trunc(angleLen / granularity()));
596 qreal degree = 0.0;
597 int iPoint = 0;
598 bool perfectMatch = false;
599
600 while (degree <= angleLen) {
601 poly[iPoint] = pointOnEllipse(drawPosition, startAngle + degree);
602 // qDebug() << degree << angleLen << poly[ iPoint ];
604 degree += granularity();
605 ++iPoint;
606 }
607 // if necessary add one more point to fill the last small gap
608 if (!perfectMatch) {
609 poly[iPoint] = pointOnEllipse(drawPosition, startAngle + angleLen);
610
611 // add the center point of the piece
612 poly.append(drawPosition.center());
613 } else {
614 poly[iPoint] = drawPosition.center();
615 }
616 // find the value and paint it
617 // fix value position
618 d->reverseMapper.addPolygon(index.row(), index.column(), poly);
619
620 painter->drawPolygon(poly);
621 }
622}
623
624// calculate the position points for the label and pass them to addLabel()
625void PieDiagram::addSliceLabel(LabelPaintCache *lpc, const QRectF &drawPosition, uint slice)
626{
627 const qreal angleLen = d->angleLens[slice];
628 const qreal startAngle = d->startAngles[slice];
629 const QModelIndex index(model()->index(0, slice, rootIndex())); // checked
630 const qreal sum = valueTotals();
631
632 // Position points are calculated relative to the slice.
633 // They are calculated as if the slice was 'standing' on its tip and the rim was up,
634 // so North is the middle (also highest part) of the rim and South is the tip of the slice.
635
636 const QPointF south = drawPosition.center();
637 const QPointF southEast = south;
638 const QPointF southWest = south;
639 const QPointF north = pointOnEllipse(drawPosition, startAngle + angleLen / 2.0);
640
641 const QPointF northEast = pointOnEllipse(drawPosition, startAngle);
642 const QPointF northWest = pointOnEllipse(drawPosition, startAngle + angleLen);
643 QPointF center = (south + north) / 2.0;
644 const QPointF east = (south + northEast) / 2.0;
645 const QPointF west = (south + northWest) / 2.0;
646
648 qreal topAngle = startAngle - 90;
649 if (topAngle < 0.0) {
650 topAngle += 360.0;
651 }
652
653 points.setDegrees(KDChartEnums::PositionEast, topAngle);
654 points.setDegrees(KDChartEnums::PositionNorthEast, topAngle);
655 points.setDegrees(KDChartEnums::PositionWest, topAngle + angleLen);
657 points.setDegrees(KDChartEnums::PositionCenter, topAngle + angleLen / 2.0);
658 points.setDegrees(KDChartEnums::PositionNorth, topAngle + angleLen / 2.0);
659
661 if (autoRotateLabels()) {
662 favoriteTextAngle = -(startAngle + angleLen / 2) + 90.0;
663 while (favoriteTextAngle <= 0.0) {
664 favoriteTextAngle += 360.0;
665 }
666 // flip the label when upside down
667 if (favoriteTextAngle > 90.0 && favoriteTextAngle < 270.0) {
669 }
670 // negative angles can have special meaning in addLabel; otherwise they work fine
671 if (favoriteTextAngle <= 0.0) {
672 favoriteTextAngle += 360.0;
673 }
674 }
675
676 d->addLabel(lpc, index, nullptr, points, Position::Center, Position::Center,
678}
679
680static bool doSpansOverlap(qreal s1Start, qreal s1End, qreal s2Start, qreal s2End)
681{
682 if (s1Start < s2Start) {
683 return s1End >= s2Start;
684 } else {
685 return s1Start <= s2End;
686 }
687}
688
689static bool doArcsOverlap(qreal a1Start, qreal a1End, qreal a2Start, qreal a2End)
690{
691 Q_ASSERT(a1Start >= 0 && a1Start <= 360 && a1End >= 0 && a1End <= 360 && a2Start >= 0 && a2Start <= 360 && a2End >= 0 && a2End <= 360);
692 // all of this could probably be done better...
693 if (a1End < a1Start) {
694 a1End += 360;
695 }
696 if (a2End < a2Start) {
697 a2End += 360;
698 }
699
700 if (doSpansOverlap(a1Start, a1End, a2Start, a2End)) {
701 return true;
702 }
703 if (a1Start > a2Start) {
704 return doSpansOverlap(a1Start - 360.0, a1End - 360.0, a2Start, a2End);
705 } else {
706 return doSpansOverlap(a1Start + 360.0, a1End + 360.0, a2Start, a2End);
707 }
708}
709
717void PieDiagram::draw3DEffect(QPainter *painter, const QRectF &drawPosition, uint slice)
718{
719 const QModelIndex index(model()->index(0, slice, rootIndex())); // checked
721 if (!threeDAttrs.isEnabled()) {
722 return;
723 }
724
725 // NOTE: We cannot optimize away drawing some of the effects (even
726 // when not exploding), because some of the pies might be left out
727 // in future versions which would make some of the normally hidden
728 // pies visible. Complex hidden-line algorithms would be much more
729 // expensive than just drawing for nothing.
730
731 // No need to save the brush, will be changed on return from this
732 // method anyway.
733 const QBrush brush = this->brush(model()->index(0, slice, rootIndex())); // checked
734 if (threeDAttrs.useShadowColors()) {
735 painter->setBrush(QBrush(brush.color().darker()));
736 } else {
737 painter->setBrush(brush);
738 }
739
740 qreal startAngle = d->startAngles[slice];
741 qreal endAngle = startAngle + d->angleLens[slice];
742 // Normalize angles
743 while (startAngle >= 360)
744 startAngle -= 360;
745 while (endAngle >= 360)
746 endAngle -= 360;
747 Q_ASSERT(startAngle >= 0 && startAngle <= 360);
748 Q_ASSERT(endAngle >= 0 && endAngle <= 360);
749
750 // positive pie height: absolute value
751 // negative pie height: relative value
752 const int depth = threeDAttrs.depth() >= 0.0 ? threeDAttrs.depth() : -threeDAttrs.depth() / 100.0 * drawPosition.height();
753
754 if (startAngle == endAngle || startAngle == endAngle - 360) { // full circle
755 draw3dOuterRim(painter, drawPosition, depth, 180, 360);
756 } else {
757 if (doArcsOverlap(startAngle, endAngle, 180, 360)) {
758 draw3dOuterRim(painter, drawPosition, depth, startAngle, endAngle);
759 }
760
761 if (startAngle >= 270 || startAngle <= 90) {
762 draw3dCutSurface(painter, drawPosition, depth, startAngle);
763 }
764 if (endAngle >= 90 && endAngle <= 270) {
765 draw3dCutSurface(painter, drawPosition, depth, endAngle);
766 }
767 }
768}
769
779void PieDiagram::draw3dCutSurface(QPainter *painter,
780 const QRectF &rect,
781 qreal threeDHeight,
782 qreal angle)
783{
784 QPolygonF poly(4);
785 const QPointF center = rect.center();
786 const QPointF circlePoint = pointOnEllipse(rect, angle);
787 poly[0] = center;
788 poly[1] = circlePoint;
790 poly[3] = QPointF(center.x(), center.y() + threeDHeight);
791 // TODO: add polygon to ReverseMapper
792 painter->drawPolygon(poly);
793}
794
804void PieDiagram::draw3dOuterRim(QPainter *painter,
805 const QRectF &rect,
806 qreal threeDHeight,
807 qreal startAngle,
808 qreal endAngle)
809{
810 // Start with getting the points for the inner arc.
811 if (endAngle < startAngle) {
812 endAngle += 360;
813 }
814 startAngle = qMax(startAngle, qreal(180.0));
815 endAngle = qMin(endAngle, qreal(360.0));
816
817 int numHalfPoints = trunc((endAngle - startAngle) / granularity()) + 1;
818 if (numHalfPoints < 2) {
819 return;
820 }
821
823
825 int iPoint = 0;
826 bool perfectMatch = false;
827 while (degree >= startAngle) {
828 poly[numHalfPoints - iPoint - 1] = pointOnEllipse(rect, degree);
829
830 perfectMatch = (degree == startAngle);
831 degree -= granularity();
832 ++iPoint;
833 }
834 // if necessary add one more point to fill the last small gap
835 if (!perfectMatch) {
836 poly.prepend(pointOnEllipse(rect, startAngle));
838 }
839
840 poly.resize(numHalfPoints * 2);
841
842 // Now copy these arcs again into the final array, but in the
843 // opposite direction and moved down by the 3D height.
844 for (int i = numHalfPoints - 1; i >= 0; --i) {
847 poly[numHalfPoints * 2 - i - 1] = pointOnFirstArc;
848 }
849
850 // TODO: Add polygon to ReverseMapper
851 painter->drawPolygon(poly);
852}
853
860uint PieDiagram::findSliceAt(qreal angle, int colCount)
861{
862 for (int i = 0; i < colCount; ++i) {
863 qreal endseg = d->startAngles[i] + d->angleLens[i];
864 if (d->startAngles[i] <= angle && endseg >= angle) {
865 return i;
866 }
867 }
868
869 // If we have not found it, try wrap around
870 // but only if the current searched angle is < 360 degree
871 if (angle < 360)
872 return findSliceAt(angle + 360, colCount);
873 // otherwise - what ever went wrong - we return 0
874 return 0;
875}
876
883uint PieDiagram::findLeftSlice(uint slice, int colCount)
884{
885 if (slice == 0) {
886 if (colCount > 1) {
887 return colCount - 1;
888 } else {
889 return 0;
890 }
891 } else {
892 return slice - 1;
893 }
894}
895
902uint PieDiagram::findRightSlice(uint slice, int colCount)
903{
904 int rightSlice = slice + 1;
905 if (rightSlice == colCount) {
906 rightSlice = 0;
907 }
908 return rightSlice;
909}
910
915QPointF PieDiagram::pointOnEllipse(const QRectF &boundingBox, qreal angle)
916{
917 qreal angleRad = DEGTORAD(angle);
920 qreal posX = cosAngle * boundingBox.width() / 2.0;
921 qreal posY = sinAngle * boundingBox.height() / 2.0;
922 return QPointF(posX + boundingBox.center().x(),
923 posY + boundingBox.center().y());
924}
925
926/*virtual*/
928{
929 if (!model())
930 return 0;
931 const int colCount = columnCount();
932 qreal total = 0.0;
933 Q_ASSERT(model()->rowCount() >= 1);
934 for (int j = 0; j < colCount; ++j) {
935 total += qAbs(model()->data(model()->index(0, j, rootIndex())).toReal()); // checked
936 }
937 return total;
938}
939
940/*virtual*/
942{
943 return model() ? model()->columnCount(rootIndex()) : 0.0;
944}
945
946/*virtual*/
948{
949 return 1;
950}
static bool doArcsOverlap(qreal a1Start, qreal a1End, qreal a2Start, qreal a2End)
static QLineF labelAttachmentLine(const QPointF &center, const QPointF &start, const QPainterPath &label)
static QPolygonF polygonFromPainterPath(const QPainterPath &pp)
static qreal normProjection(const QLineF &l1, const QLineF &l2)
static int wraparound(int i, int size)
static bool doSpansOverlap(qreal s1Start, qreal s1End, qreal s2Start, qreal s2End)
virtual bool checkInvariants(bool justReturnTheStatus=false) const
Base class for any diagram type.
ThreeDPieAttributes threeDPieAttributes() const
const PolarCoordinatePlane * polarCoordinatePlane() const
Stores information about painting diagrams.
void setPainter(QPainter *painter)
A set of attributes controlling the appearance of pie charts.
PieDiagram defines a common pie diagram.
bool isLabelCollisionAvoidanceEnabled() const
Return whether overlapping labels will be moved to until they don't overlap anymore.
LabelDecorations labelDecorations() const
Return the decorations to be painted around data labels.
void setLabelDecorations(LabelDecorations decorations)
Set the decorations to be painted around data labels according to decorations.
qreal valueTotals() const override
void resizeEvent(QResizeEvent *) override
PieDiagram(QWidget *parent=nullptr, PolarCoordinatePlane *plane=nullptr)
void paint(PaintContext *paintContext) override
qreal numberOfGridRings() const override
virtual PieDiagram * clone() const
void paintEvent(QPaintEvent *) override
void resize(const QSizeF &area) override
@ LineFromSliceDecoration
A line is drawn from the pie slice to its label.
@ FrameDecoration
A rectangular frame is painted around the label text.
qreal numberOfValuesPerDataset() const override
void setLabelCollisionAvoidanceEnabled(bool enabled)
const QPair< QPointF, QPointF > calculateDataBoundaries() const override
Stores the absolute target points of a Position.
static const Position & Center
A set of text attributes.
virtual int columnCount(const QModelIndex &parent) const const=0
QAbstractItemModel * model() const const
QModelIndex rootIndex() const const
QWidget * viewport() const const
const QColor & color() const const
QColor darker(int factor) const const
qreal dx() const const
qreal dy() const const
qreal length() const const
QPointF p1() const const
void setLength(qreal length)
void setP2(const QPointF &p2)
T qobject_cast(QObject *object)
int depth() const const
void drawEllipse(const QRectF &rectangle)
void drawPolygon(const QPointF *points, int pointCount, Qt::FillRule fillRule)
void setBrush(const QBrush &brush)
void setPen(const QColor &color)
void setRenderHint(QPainter::RenderHint hint, bool on)
QPainterPath::Element elementAt(int index) const const
int elementCount() const const
void translate(qreal dx, qreal dy)
void setColor(const QColor &color)
int height() const const
int left() const const
int top() const const
int width() const const
qreal bottom() const const
qreal left() const const
qreal right() const const
QSizeF size() const const
qreal top() const const
QTextStream & center(QTextStream &stream)
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
void append(const T &value)
QVector< T > & fill(const T &value, int size)
QRect contentsRect() const const

© 2001 Klarälvdalens Datakonsult AB (KDAB)
"The Qt, C++ and OpenGL Experts"
https://www.kdab.com/
https://www.kdab.com/development-resources/qt-tools/kd-chart/
Generated on Fri Apr 26 2024 00:04:57 for KD Chart API Documentation by doxygen 1.9.8