KD Chart 2  [rev.2.5.1]
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Macros Pages
KDChartPieDiagram.cpp
Go to the documentation of this file.
1 /****************************************************************************
2 ** Copyright (C) 2001-2013 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 * 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 }

Klarälvdalens Datakonsult AB (KDAB)
Qt-related services and products
http://www.kdab.com/
http://www.kdab.com/products/kd-chart/