KChart

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

KDE's Doxygen guidelines are available online.