KChart

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

KDE's Doxygen guidelines are available online.