KD Chart API Documentation  3.1
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"
20 #include "KDChartPieAttributes.h"
21 #include "KDChartPolarCoordinatePlane_p.h"
23 
24 #include <KDABLibFakes>
25 
26 using namespace KDChart;
27 
28 PieDiagram::Private::Private()
29  : labelDecorations(PieDiagram::NoDecoration)
30 {
31 }
32 
33 PieDiagram::Private::~Private()
34 {
35 }
36 
37 #define d d_func()
38 
39 PieDiagram::PieDiagram(QWidget *parent, PolarCoordinatePlane *plane)
40  : AbstractPieDiagram(new Private(), parent, plane)
41 {
42  init();
43 }
44 
46 {
47 }
48 
49 void PieDiagram::init()
50 {
51 }
52 
57 {
58  return new PieDiagram(new Private(*d));
59 }
60 
61 void PieDiagram::setLabelDecorations(LabelDecorations decorations)
62 {
63  d->labelDecorations = decorations;
64 }
65 
66 PieDiagram::LabelDecorations PieDiagram::labelDecorations() const
67 {
68  return d->labelDecorations;
69 }
70 
72 {
73  d->isCollisionAvoidanceEnabled = enabled;
74 }
75 
77 {
78  return d->isCollisionAvoidanceEnabled;
79 }
80 
81 const QPair<QPointF, QPointF> PieDiagram::calculateDataBoundaries() const
82 {
83  if (!checkInvariants(true) || model()->rowCount() < 1)
84  return QPair<QPointF, QPointF>(QPointF(0, 0), QPointF(0, 0));
85 
86  const PieAttributes attrs(pieAttributes());
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 
105 void PieDiagram::paintEvent(QPaintEvent *)
106 {
107  QPainter painter(viewport());
108  PaintContext ctx;
109  ctx.setPainter(&painter);
110  ctx.setRectangle(QRectF(0, 0, width(), height()));
111  paint(&ctx);
112 }
113 
114 void PieDiagram::resizeEvent(QResizeEvent *)
115 {
116 }
117 
118 void PieDiagram::resize(const QSizeF &)
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 
133 void 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
151  atLeastOneValue = atLeastOneValue || isOk;
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 
166 void PieDiagram::calcPieSize(const QRectF &contentsRect)
167 {
168  d->size = qMin(contentsRect.width(), contentsRect.height());
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.
185 QRectF PieDiagram::twoDPieRect(const QRectF &contentsRect, const ThreeDPieAttributes &threeDAttrs) const
186 {
187  QRectF pieRect;
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
194  qreal sizeFor3DEffect = 0.0;
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
202  sizeFor3DEffect = threeDAttrs.depth();
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  }
209  qreal y = (contentsRect.height() - height - sizeFor3DEffect) / 2.0;
210 
211  pieRect = QRectF(contentsRect.left() + x, contentsRect.top() + y, d->size, height);
212  }
213  return pieRect;
214 }
215 
216 void 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 
225  const ThreeDPieAttributes threeDAttrs(threeDPieAttributes());
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 
254  QRectF textBoundingRect;
255  d->paintDataValueTextsAndMarkers(paintContext, d->labelPaintCache, false, true,
256  &textBoundingRect);
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 
281 static 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 
294 void 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;
308  QVector<qreal> offsets;
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();
331  ta.setPen(QPen(Qt::white));
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 
355 static QPolygonF polygonFromPainterPath(const QPainterPath &pp)
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
367 static 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 
373 static 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 
429 void 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 
440  const ThreeDPieAttributes threeDAttrs(threeDPieAttributes());
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);
449  int currentLeftSlice = backmostSlice;
450  int currentRightSlice = backmostSlice;
451 
452  drawSlice(paintContext->painter(), pieRect, backmostSlice);
453 
454  if (backmostSlice == frontmostSlice) {
455  const int rightmostSlice = findSliceAt(0, colCount);
456  const int leftmostSlice = findSliceAt(180, colCount);
457 
458  if (backmostSlice == leftmostSlice) {
459  currentLeftSlice = findLeftSlice(currentLeftSlice, colCount);
460  }
461  if (backmostSlice == rightmostSlice) {
462  currentRightSlice = findRightSlice(currentRightSlice, colCount);
463  }
464  }
465 
466  while (currentLeftSlice != frontmostSlice) {
467  if (currentLeftSlice != backmostSlice) {
468  drawSlice(paintContext->painter(), pieRect, currentLeftSlice);
469  }
470  currentLeftSlice = findLeftSlice(currentLeftSlice, colCount);
471  }
472 
473  while (currentRightSlice != frontmostSlice) {
474  if (currentRightSlice != backmostSlice) {
475  drawSlice(paintContext->painter(), pieRect, currentRightSlice);
476  }
477  currentRightSlice = findRightSlice(currentRightSlice, colCount);
478  }
479 
480  // if the backmost slice is not the frontmost slice, we draw the frontmost one last
481  if (backmostSlice != frontmostSlice || !threeDPieAttributes().isEnabled()) {
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);
492  for (const LabelPaintInfo &pi : qAsConst(d->labelPaintCache.paintReplay)) {
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 
517 QRectF 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 
522  QRectF adjustedDrawPosition = drawPosition;
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 
529  adjustedDrawPosition.translate(explodeDistance * cos(explodeAngle),
530  explodeDistance * -sin(explodeAngle));
531  }
532  return adjustedDrawPosition;
533 }
534 
543 void 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 
561 void 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));
569  const ThreeDPieAttributes threeDAttrs(threeDPieAttributes(index));
570 
571  painter->setRenderHint(QPainter::Antialiasing);
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()) {
580  pen.setColor(Qt::black);
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.
589  QPolygonF poly(drawPosition);
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()));
595  QPolygonF poly(arcPoints + 2);
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 ];
603  perfectMatch = (degree == angleLen);
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()
625 void 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 
647  PositionPoints points(center, northWest, north, northEast, east, southEast, south, southWest, west);
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);
656  points.setDegrees(KDChartEnums::PositionNorthWest, topAngle + angleLen);
657  points.setDegrees(KDChartEnums::PositionCenter, topAngle + angleLen / 2.0);
658  points.setDegrees(KDChartEnums::PositionNorth, topAngle + angleLen / 2.0);
659 
660  qreal favoriteTextAngle = 0.0;
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) {
668  favoriteTextAngle = favoriteTextAngle - 180.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,
677  angleLen * sum / 360, favoriteTextAngle);
678 }
679 
680 static 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 
689 static 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 
717 void PieDiagram::draw3DEffect(QPainter *painter, const QRectF &drawPosition, uint slice)
718 {
719  const QModelIndex index(model()->index(0, slice, rootIndex())); // checked
720  const ThreeDPieAttributes threeDAttrs(threeDPieAttributes(index));
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 
779 void 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;
789  poly[2] = QPointF(circlePoint.x(), circlePoint.y() + threeDHeight);
790  poly[3] = QPointF(center.x(), center.y() + threeDHeight);
791  // TODO: add polygon to ReverseMapper
792  painter->drawPolygon(poly);
793 }
794 
804 void 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 
822  QPolygonF poly(numHalfPoints);
823 
824  qreal degree = endAngle;
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));
837  ++numHalfPoints;
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) {
845  QPointF pointOnFirstArc(poly[i]);
846  pointOnFirstArc.setY(pointOnFirstArc.y() + threeDHeight);
847  poly[numHalfPoints * 2 - i - 1] = pointOnFirstArc;
848  }
849 
850  // TODO: Add polygon to ReverseMapper
851  painter->drawPolygon(poly);
852 }
853 
860 uint 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 
883 uint 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 
902 uint 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 
915 QPointF PieDiagram::pointOnEllipse(const QRectF &boundingBox, qreal angle)
916 {
917  qreal angleRad = DEGTORAD(angle);
918  qreal cosAngle = cos(angleRad);
919  qreal sinAngle = -sin(angleRad);
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)
#define d
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
virtual QBrush threeDBrush(const QBrush &brush, const QRectF &rect) const
Stores information about painting diagrams.
void setPainter(QPainter *painter)
void setRectangle(const QRectF &rect)
const QRectF rectangle() const
QPainter * painter() const
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.
void setPen(const QPen &pen)

© 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 by doxygen 1.9.1