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

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/