KD Chart 2
[rev.2.5]
|
00001 /**************************************************************************** 00002 ** Copyright (C) 2001-2012 Klaralvdalens Datakonsult AB. All rights reserved. 00003 ** 00004 ** This file is part of the KD Chart library. 00005 ** 00006 ** Licensees holding valid commercial KD Chart licenses may use this file in 00007 ** accordance with the KD Chart Commercial License Agreement provided with 00008 ** the Software. 00009 ** 00010 ** 00011 ** This file may be distributed and/or modified under the terms of the 00012 ** GNU General Public License version 2 and version 3 as published by the 00013 ** Free Software Foundation and appearing in the file LICENSE.GPL.txt included. 00014 ** 00015 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE 00016 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. 00017 ** 00018 ** Contact info@kdab.com if any conditions of this licensing are not 00019 ** clear to you. 00020 ** 00021 **********************************************************************/ 00022 00023 #include <QDebug> 00024 #include <QPainter> 00025 #include <QStack> 00026 00027 #include "KDChartPieDiagram.h" 00028 #include "KDChartPieDiagram_p.h" 00029 00030 #include "KDChartAttributesModel.h" 00031 #include "KDChartPaintContext.h" 00032 #include "KDChartPieAttributes.h" 00033 #include "KDChartPolarCoordinatePlane_p.h" 00034 #include "KDChartThreeDPieAttributes.h" 00035 #include "KDChartPainterSaver_p.h" 00036 #include "KDChartDataValueAttributes.h" 00037 00038 #include <KDABLibFakes> 00039 00040 00041 using namespace KDChart; 00042 00043 PieDiagram::Private::Private() 00044 : labelDecorations( PieDiagram::NoDecoration ), 00045 isCollisionAvoidanceEnabled( false ) 00046 { 00047 } 00048 00049 PieDiagram::Private::~Private() {} 00050 00051 #define d d_func() 00052 00053 PieDiagram::PieDiagram( QWidget* parent, PolarCoordinatePlane* plane ) : 00054 AbstractPieDiagram( new Private(), parent, plane ) 00055 { 00056 init(); 00057 } 00058 00059 PieDiagram::~PieDiagram() 00060 { 00061 } 00062 00063 void PieDiagram::init() 00064 { 00065 } 00066 00070 PieDiagram * PieDiagram::clone() const 00071 { 00072 return new PieDiagram( new Private( *d ) ); 00073 } 00074 00075 void PieDiagram::setLabelDecorations( LabelDecorations decorations ) 00076 { 00077 d->labelDecorations = decorations; 00078 } 00079 00080 PieDiagram::LabelDecorations PieDiagram::labelDecorations() const 00081 { 00082 return d->labelDecorations; 00083 } 00084 00085 void PieDiagram::setLabelCollisionAvoidanceEnabled( bool enabled ) 00086 { 00087 d->isCollisionAvoidanceEnabled = enabled; 00088 } 00089 00090 bool PieDiagram::isLabelCollisionAvoidanceEnabled() const 00091 { 00092 return d->isCollisionAvoidanceEnabled; 00093 } 00094 00095 const QPair<QPointF, QPointF> PieDiagram::calculateDataBoundaries () const 00096 { 00097 if ( !checkInvariants( true ) || model()->rowCount() < 1 ) return QPair<QPointF, QPointF>( QPointF( 0, 0 ), QPointF( 0, 0 ) ); 00098 00099 const PieAttributes attrs( pieAttributes() ); 00100 00101 QPointF bottomLeft( QPointF( 0, 0 ) ); 00102 QPointF topRight; 00103 // If we explode, we need extra space for the slice that has the largest explosion distance. 00104 if ( attrs.explode() ) { 00105 const int colCount = columnCount(); 00106 qreal maxExplode = 0.0; 00107 for ( int j = 0; j < colCount; ++j ) { 00108 const PieAttributes columnAttrs( pieAttributes( model()->index( 0, j, rootIndex() ) ) ); // checked 00109 maxExplode = qMax( maxExplode, columnAttrs.explodeFactor() ); 00110 } 00111 topRight = QPointF( 1.0 + maxExplode, 1.0 + maxExplode ); 00112 } else { 00113 topRight = QPointF( 1.0, 1.0 ); 00114 } 00115 return QPair<QPointF, QPointF> ( bottomLeft, topRight ); 00116 } 00117 00118 00119 void PieDiagram::paintEvent( QPaintEvent* ) 00120 { 00121 QPainter painter ( viewport() ); 00122 PaintContext ctx; 00123 ctx.setPainter ( &painter ); 00124 ctx.setRectangle( QRectF ( 0, 0, width(), height() ) ); 00125 paint ( &ctx ); 00126 } 00127 00128 void PieDiagram::resizeEvent( QResizeEvent* ) 00129 { 00130 } 00131 00132 void PieDiagram::resize( const QSizeF& ) 00133 { 00134 } 00135 00136 void PieDiagram::paint( PaintContext* ctx ) 00137 { 00138 // Painting is a two stage process 00139 // In the first stage we figure out how much space is needed 00140 // for text labels. 00141 // In the second stage, we make use of that information and 00142 // perform the actual painting. 00143 placeLabels( ctx ); 00144 paintInternal( ctx ); 00145 } 00146 00147 void PieDiagram::calcSliceAngles() 00148 { 00149 // determine slice positions and sizes 00150 const qreal sum = valueTotals(); 00151 const qreal sectorsPerValue = 360.0 / sum; 00152 const PolarCoordinatePlane* plane = polarCoordinatePlane(); 00153 qreal currentValue = plane ? plane->startPosition() : 0.0; 00154 00155 const int colCount = columnCount(); 00156 d->startAngles.resize( colCount ); 00157 d->angleLens.resize( colCount ); 00158 00159 bool atLeastOneValue = false; // guard against completely empty tables 00160 for ( int iColumn = 0; iColumn < colCount; ++iColumn ) { 00161 bool isOk; 00162 const qreal cellValue = qAbs( model()->data( model()->index( 0, iColumn, rootIndex() ) ) // checked 00163 .toReal( &isOk ) ); 00164 // toReal() returns 0.0 if there was no value or a non-numeric value 00165 atLeastOneValue = atLeastOneValue || isOk; 00166 00167 d->startAngles[ iColumn ] = currentValue; 00168 d->angleLens[ iColumn ] = cellValue * sectorsPerValue; 00169 00170 currentValue = d->startAngles[ iColumn ] + d->angleLens[ iColumn ]; 00171 } 00172 00173 // If there was no value at all, this is the sign for other code to bail out 00174 if ( !atLeastOneValue ) { 00175 d->startAngles.clear(); 00176 d->angleLens.clear(); 00177 } 00178 } 00179 00180 void PieDiagram::calcPieSize( const QRectF &contentsRect ) 00181 { 00182 d->size = qMin( contentsRect.width(), contentsRect.height() ); 00183 00184 // if any slice explodes, the whole pie needs additional space so we make the basic size smaller 00185 qreal maxExplode = 0.0; 00186 const int colCount = columnCount(); 00187 for ( int j = 0; j < colCount; ++j ) { 00188 const PieAttributes columnAttrs( pieAttributes( model()->index( 0, j, rootIndex() ) ) ); // checked 00189 maxExplode = qMax( maxExplode, columnAttrs.explodeFactor() ); 00190 } 00191 d->size /= ( 1.0 + 1.0 * maxExplode ); 00192 00193 if ( d->size < 0.0 ) { 00194 d->size = 0; 00195 } 00196 } 00197 00198 // this is the rect of the top surface of the pie, i.e. excluding the "3D" rim effect. 00199 QRectF PieDiagram::twoDPieRect( const QRectF &contentsRect, const ThreeDPieAttributes& threeDAttrs ) const 00200 { 00201 QRectF pieRect; 00202 if ( !threeDAttrs.isEnabled() ) { 00203 qreal x = ( contentsRect.width() - d->size ) / 2.0; 00204 qreal y = ( contentsRect.height() - d->size ) / 2.0; 00205 pieRect = QRectF( contentsRect.left() + x, contentsRect.top() + y, d->size, d->size ); 00206 } else { 00207 // threeD: width is the maximum possible width; height is 1/2 of that 00208 qreal sizeFor3DEffect = 0.0; 00209 00210 qreal x = ( contentsRect.width() - d->size ) / 2.0; 00211 qreal height = d->size; 00212 // make sure that the height plus the threeDheight is not more than the 00213 // available size 00214 if ( threeDAttrs.depth() >= 0.0 ) { 00215 // positive pie height: absolute value 00216 sizeFor3DEffect = threeDAttrs.depth(); 00217 height = d->size - sizeFor3DEffect; 00218 } else { 00219 // negative pie height: relative value 00220 sizeFor3DEffect = - threeDAttrs.depth() / 100.0 * height; 00221 height = d->size - sizeFor3DEffect; 00222 } 00223 qreal y = ( contentsRect.height() - height - sizeFor3DEffect ) / 2.0; 00224 00225 pieRect = QRectF( contentsRect.left() + x, contentsRect.top() + y, d->size, height ); 00226 } 00227 return pieRect; 00228 } 00229 00230 void PieDiagram::placeLabels( PaintContext* paintContext ) 00231 { 00232 if ( !checkInvariants(true) || model()->rowCount() < 1 ) { 00233 return; 00234 } 00235 if ( paintContext->rectangle().isEmpty() || valueTotals() == 0.0 ) { 00236 return; 00237 } 00238 00239 const ThreeDPieAttributes threeDAttrs( threeDPieAttributes() ); 00240 const int colCount = columnCount(); 00241 00242 d->reverseMapper.clear(); // on first call, this sets up the internals of the ReverseMapper. 00243 00244 calcSliceAngles(); 00245 if ( d->startAngles.isEmpty() ) { 00246 return; 00247 } 00248 00249 calcPieSize( paintContext->rectangle() ); 00250 00251 // keep resizing the pie until the labels and the pie fit into paintContext->rectangle() 00252 00253 bool tryAgain = true; 00254 while ( tryAgain ) { 00255 tryAgain = false; 00256 00257 QRectF pieRect = twoDPieRect( paintContext->rectangle(), threeDAttrs ); 00258 d->forgetAlreadyPaintedDataValues(); 00259 d->labelPaintCache.clear(); 00260 00261 for ( int slice = 0; slice < colCount; slice++ ) { 00262 if ( d->angleLens[ slice ] != 0.0 ) { 00263 const QRectF explodedPieRect = explodedDrawPosition( pieRect, slice ); 00264 addSliceLabel( &d->labelPaintCache, explodedPieRect, slice ); 00265 } 00266 } 00267 00268 QRectF textBoundingRect; 00269 d->paintDataValueTextsAndMarkers( paintContext, d->labelPaintCache, false, true, 00270 &textBoundingRect ); 00271 if ( d->isCollisionAvoidanceEnabled ) { 00272 shuffleLabels( &textBoundingRect ); 00273 } 00274 00275 if ( !textBoundingRect.isEmpty() && d->size > 0.0 ) { 00276 const QRectF &clipRect = paintContext->rectangle(); 00277 // see by how many pixels the text is clipped on each side 00278 qreal right = qMax( qreal( 0.0 ), textBoundingRect.right() - clipRect.right() ); 00279 qreal left = qMax( qreal( 0.0 ), clipRect.left() - textBoundingRect.left() ); 00280 // attention here - y coordinates in Qt are inverted compared to the convention in maths 00281 qreal top = qMax( qreal( 0.0 ), clipRect.top() - textBoundingRect.top() ); 00282 qreal bottom = qMax( qreal( 0.0 ), textBoundingRect.bottom() - clipRect.bottom() ); 00283 qreal maxOverhang = qMax( qMax( right, left ), qMax( top, bottom ) ); 00284 00285 if ( maxOverhang > 0.0 ) { 00286 // subtract 2x as much because every side only gets half of the total diameter reduction 00287 // and we have to make up for the overhang on one particular side. 00288 d->size -= qMin( d->size, maxOverhang * 2.0 ); 00289 tryAgain = true; 00290 } 00291 } 00292 } 00293 } 00294 00295 static int wraparound( int i, int size ) 00296 { 00297 while ( i < 0 ) { 00298 i += size; 00299 } 00300 while ( i >= size ) { 00301 i -= size; 00302 } 00303 return i; 00304 } 00305 00306 //#define SHUFFLE_DEBUG 00307 00308 void PieDiagram::shuffleLabels( QRectF* textBoundingRect ) 00309 { 00310 // things that could be improved here: 00311 // - use a variable number (chosen using angle information) of neighbors to check 00312 // - try harder to arrange the labels to look nice 00313 00314 // ideas: 00315 // - leave labels that don't collide alone (only if they their offset is zero) 00316 // - use a graphics view for collision detection 00317 00318 LabelPaintCache& lpc = d->labelPaintCache; 00319 const int n = lpc.paintReplay.size(); 00320 bool modified = false; 00321 qreal direction = 5.0; 00322 QVector< qreal > offsets; 00323 offsets.fill( 0.0, n ); 00324 00325 for ( bool lastRoundModified = true; lastRoundModified; ) { 00326 lastRoundModified = false; 00327 00328 for ( int i = 0; i < n; i++ ) { 00329 const int neighborsToCheck = qMax( 10, lpc.paintReplay.size() - 1 ); 00330 const int minComp = wraparound( i - neighborsToCheck / 2, n ); 00331 const int maxComp = wraparound( i + ( neighborsToCheck + 1 ) / 2, n ); 00332 00333 QPainterPath& path = lpc.paintReplay[ i ].labelArea; 00334 00335 for ( int j = minComp; j != maxComp; j = wraparound( j + 1, n ) ) { 00336 if ( i == j ) { 00337 continue; 00338 } 00339 QPainterPath& otherPath = lpc.paintReplay[ j ].labelArea; 00340 00341 while ( ( offsets[ i ] + direction > 0 ) && otherPath.intersects( path ) ) { 00342 #ifdef SHUFFLE_DEBUG 00343 qDebug() << "collision involving" << j << "and" << i << " -- n =" << n; 00344 TextAttributes ta = lpc.paintReplay[ i ].attrs.textAttributes(); 00345 ta.setPen( QPen( Qt::white ) ); 00346 lpc.paintReplay[ i ].attrs.setTextAttributes( ta ); 00347 #endif 00348 uint slice = lpc.paintReplay[ i ].index.column(); 00349 qreal angle = DEGTORAD( d->startAngles[ slice ] + d->angleLens[ slice ] / 2.0 ); 00350 qreal dx = cos( angle ) * direction; 00351 qreal dy = -sin( angle ) * direction; 00352 offsets[ i ] += direction; 00353 path.translate( dx, dy ); 00354 lastRoundModified = true; 00355 } 00356 } 00357 } 00358 direction *= -1.07; // this can "overshoot", but avoids getting trapped in local minimums 00359 modified = modified || lastRoundModified; 00360 } 00361 00362 if ( modified ) { 00363 for ( int i = 0; i < lpc.paintReplay.size(); i++ ) { 00364 *textBoundingRect |= lpc.paintReplay[ i ].labelArea.boundingRect(); 00365 } 00366 } 00367 } 00368 00369 static QPolygonF polygonFromPainterPath( const QPainterPath &pp ) 00370 { 00371 QPolygonF ret; 00372 for ( int i = 0; i < pp.elementCount(); i++ ) { 00373 const QPainterPath::Element& el = pp.elementAt( i ); 00374 Q_ASSERT( el.type == QPainterPath::MoveToElement || el.type == QPainterPath::LineToElement ); 00375 ret.append( el ); 00376 } 00377 return ret; 00378 } 00379 00380 // you can call it "normalizedProjectionLength" if you like 00381 static qreal normProjection( const QLineF &l1, const QLineF &l2 ) 00382 { 00383 const qreal dotProduct = l1.dx() * l2.dx() + l1.dy() * l2.dy(); 00384 return qAbs( dotProduct / ( l1.length() * l2.length() ) ); 00385 } 00386 00387 static QLineF labelAttachmentLine( const QPointF ¢er, const QPointF &start, const QPainterPath &label ) 00388 { 00389 Q_ASSERT ( label.elementCount() == 5 ); 00390 00391 // start is assumed to lie on the outer rim of the slice(!), making it possible to derive the 00392 // radius of the pie 00393 const qreal pieRadius = QLineF( center, start ).length(); 00394 00395 // don't draw a line at all when the label is connected to its slice due to at least one of its 00396 // corners falling inside the slice. 00397 for ( int i = 0; i < 4; i++ ) { // point 4 is just a duplicate of point 0 00398 if ( QLineF( label.elementAt( i ), center ).length() < pieRadius ) { 00399 return QLineF(); 00400 } 00401 } 00402 00403 // find the closest edge in the polygon, and its two neighbors 00404 QPointF closeCorners[3]; 00405 { 00406 QPointF closest = QPointF( 1000000, 1000000 ); 00407 int closestIndex = 0; // better misbehave than crash 00408 for ( int i = 0; i < 4; i++ ) { // point 4 is just a duplicate of point 0 00409 QPointF p = label.elementAt( i ); 00410 if ( QLineF( p, center ).length() < QLineF( closest, center ).length() ) { 00411 closest = p; 00412 closestIndex = i; 00413 } 00414 } 00415 00416 closeCorners[ 0 ] = label.elementAt( wraparound( closestIndex - 1, 4 ) ); 00417 closeCorners[ 1 ] = closest; 00418 closeCorners[ 2 ] = label.elementAt( wraparound( closestIndex + 1, 4 ) ); 00419 } 00420 00421 QLineF edge1 = QLineF( closeCorners[ 0 ], closeCorners[ 1 ] ); 00422 QLineF edge2 = QLineF( closeCorners[ 1 ], closeCorners[ 2 ] ); 00423 QLineF connection1 = QLineF( ( closeCorners[ 0 ] + closeCorners[ 1 ] ) / 2.0, center ); 00424 QLineF connection2 = QLineF( ( closeCorners[ 1 ] + closeCorners[ 2 ] ) / 2.0, center ); 00425 QLineF ret; 00426 // prefer the connecting line meeting its edge at a more perpendicular angle 00427 if ( normProjection( edge1, connection1 ) < normProjection( edge2, connection2 ) ) { 00428 ret = connection1; 00429 } else { 00430 ret = connection2; 00431 } 00432 00433 // This tends to look a bit better than not doing it *shrug* 00434 ret.setP2( ( start + center ) / 2.0 ); 00435 00436 // make the line end at the rim of the slice (not 100% accurate because the line is not precisely radial) 00437 qreal p1Radius = QLineF( ret.p1(), center ).length(); 00438 ret.setLength( p1Radius - pieRadius ); 00439 00440 return ret; 00441 } 00442 00443 void PieDiagram::paintInternal( PaintContext* paintContext ) 00444 { 00445 // note: Not having any data model assigned is no bug 00446 // but we can not draw a diagram then either. 00447 if ( !checkInvariants( true ) || model()->rowCount() < 1 ) { 00448 return; 00449 } 00450 if ( d->startAngles.isEmpty() || paintContext->rectangle().isEmpty() || valueTotals() == 0.0 ) { 00451 return; 00452 } 00453 00454 const ThreeDPieAttributes threeDAttrs( threeDPieAttributes() ); 00455 const int colCount = columnCount(); 00456 00457 // Paint from back to front ("painter's algorithm") - first draw the backmost slice, 00458 // then the slices on the left and right from back to front, then the frontmost one. 00459 00460 QRectF pieRect = twoDPieRect( paintContext->rectangle(), threeDAttrs ); 00461 const int backmostSlice = findSliceAt( 90, colCount ); 00462 const int frontmostSlice = findSliceAt( 270, colCount ); 00463 int currentLeftSlice = backmostSlice; 00464 int currentRightSlice = backmostSlice; 00465 00466 drawSlice( paintContext->painter(), pieRect, backmostSlice ); 00467 00468 if ( backmostSlice == frontmostSlice ) { 00469 const int rightmostSlice = findSliceAt( 0, colCount ); 00470 const int leftmostSlice = findSliceAt( 180, colCount ); 00471 00472 if ( backmostSlice == leftmostSlice ) { 00473 currentLeftSlice = findLeftSlice( currentLeftSlice, colCount ); 00474 } 00475 if ( backmostSlice == rightmostSlice ) { 00476 currentRightSlice = findRightSlice( currentRightSlice, colCount ); 00477 } 00478 } 00479 00480 while ( currentLeftSlice != frontmostSlice ) { 00481 if ( currentLeftSlice != backmostSlice ) { 00482 drawSlice( paintContext->painter(), pieRect, currentLeftSlice ); 00483 } 00484 currentLeftSlice = findLeftSlice( currentLeftSlice, colCount ); 00485 } 00486 00487 while ( currentRightSlice != frontmostSlice ) { 00488 if ( currentRightSlice != backmostSlice ) { 00489 drawSlice( paintContext->painter(), pieRect, currentRightSlice ); 00490 } 00491 currentRightSlice = findRightSlice( currentRightSlice, colCount ); 00492 } 00493 00494 // if the backmost slice is not the frontmost slice, we draw the frontmost one last 00495 if ( backmostSlice != frontmostSlice || ! threeDPieAttributes().isEnabled() ) { 00496 drawSlice( paintContext->painter(), pieRect, frontmostSlice ); 00497 } 00498 00499 d->paintDataValueTextsAndMarkers( paintContext, d->labelPaintCache, false, false ); 00500 // it's safer to do this at the beginning of placeLabels, but we can save some memory here. 00501 d->forgetAlreadyPaintedDataValues(); 00502 // ### maybe move this into AbstractDiagram, also make ReverseMapper deal better with multiple polygons 00503 const QPointF center = paintContext->rectangle().center(); 00504 const PainterSaver painterSaver( paintContext->painter() ); 00505 paintContext->painter()->setBrush( Qt::NoBrush ); 00506 KDAB_FOREACH( const LabelPaintInfo &pi, d->labelPaintCache.paintReplay ) { 00507 // we expect the PainterPath to be a rectangle 00508 if ( pi.labelArea.elementCount() != 5 ) { 00509 continue; 00510 } 00511 00512 paintContext->painter()->setPen( pen( pi.index ) ); 00513 if ( d->labelDecorations & LineFromSliceDecoration ) { 00514 paintContext->painter()->drawLine( labelAttachmentLine( center, pi.markerPos, pi.labelArea ) ); 00515 } 00516 if ( d->labelDecorations & FrameDecoration ) { 00517 paintContext->painter()->drawPath( pi.labelArea ); 00518 } 00519 d->reverseMapper.addPolygon( pi.index.row(), pi.index.column(), 00520 polygonFromPainterPath( pi.labelArea ) ); 00521 } 00522 d->labelPaintCache.clear(); 00523 d->startAngles.clear(); 00524 d->angleLens.clear(); 00525 } 00526 00527 #if defined ( Q_WS_WIN) 00528 #define trunc(x) ((int)(x)) 00529 #endif 00530 00531 QRectF PieDiagram::explodedDrawPosition( const QRectF& drawPosition, uint slice ) const 00532 { 00533 const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked 00534 const PieAttributes attrs( pieAttributes( index ) ); 00535 00536 QRectF adjustedDrawPosition = drawPosition; 00537 if ( attrs.explode() ) { 00538 qreal startAngle = d->startAngles[ slice ]; 00539 qreal angleLen = d->angleLens[ slice ]; 00540 qreal explodeAngle = ( DEGTORAD( startAngle + angleLen / 2.0 ) ); 00541 qreal explodeDistance = attrs.explodeFactor() * d->size / 2.0; 00542 00543 adjustedDrawPosition.translate( explodeDistance * cos( explodeAngle ), 00544 explodeDistance * - sin( explodeAngle ) ); 00545 } 00546 return adjustedDrawPosition; 00547 } 00548 00557 void PieDiagram::drawSlice( QPainter* painter, const QRectF& drawPosition, uint slice) 00558 { 00559 // Is there anything to draw at all? 00560 if ( d->angleLens[ slice ] == 0.0 ) { 00561 return; 00562 } 00563 const QRectF adjustedDrawPosition = explodedDrawPosition( drawPosition, slice ); 00564 draw3DEffect( painter, adjustedDrawPosition, slice ); 00565 drawSliceSurface( painter, adjustedDrawPosition, slice ); 00566 } 00567 00575 void PieDiagram::drawSliceSurface( QPainter* painter, const QRectF& drawPosition, uint slice ) 00576 { 00577 // Is there anything to draw at all? 00578 const qreal angleLen = d->angleLens[ slice ]; 00579 const qreal startAngle = d->startAngles[ slice ]; 00580 const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked 00581 00582 const PieAttributes attrs( pieAttributes( index ) ); 00583 const ThreeDPieAttributes threeDAttrs( threeDPieAttributes( index ) ); 00584 00585 painter->setRenderHint ( QPainter::Antialiasing ); 00586 QBrush br = brush( index ); 00587 if ( threeDAttrs.isEnabled() ) { 00588 br = threeDAttrs.threeDBrush( br, drawPosition ); 00589 } 00590 painter->setBrush( br ); 00591 00592 QPen pen = this->pen( index ); 00593 if ( threeDAttrs.isEnabled() ) { 00594 pen.setColor( Qt::black ); 00595 } 00596 painter->setPen( pen ); 00597 00598 if ( angleLen == 360 ) { 00599 // full circle, avoid nasty line in the middle 00600 painter->drawEllipse( drawPosition ); 00601 00602 //Add polygon to Reverse mapper for showing tool tips. 00603 QPolygonF poly( drawPosition ); 00604 d->reverseMapper.addPolygon( index.row(), index.column(), poly ); 00605 } else { 00606 // draw the top of this piece 00607 // Start with getting the points for the arc. 00608 const int arcPoints = static_cast<int>(trunc( angleLen / granularity() )); 00609 QPolygonF poly( arcPoints + 2 ); 00610 qreal degree = 0.0; 00611 int iPoint = 0; 00612 bool perfectMatch = false; 00613 00614 while ( degree <= angleLen ) { 00615 poly[ iPoint ] = pointOnEllipse( drawPosition, startAngle + degree ); 00616 //qDebug() << degree << angleLen << poly[ iPoint ]; 00617 perfectMatch = ( degree == angleLen ); 00618 degree += granularity(); 00619 ++iPoint; 00620 } 00621 // if necessary add one more point to fill the last small gap 00622 if ( !perfectMatch ) { 00623 poly[ iPoint ] = pointOnEllipse( drawPosition, startAngle + angleLen ); 00624 00625 // add the center point of the piece 00626 poly.append( drawPosition.center() ); 00627 } else { 00628 poly[ iPoint ] = drawPosition.center(); 00629 } 00630 //find the value and paint it 00631 //fix value position 00632 d->reverseMapper.addPolygon( index.row(), index.column(), poly ); 00633 00634 painter->drawPolygon( poly ); 00635 } 00636 } 00637 00638 // calculate the position points for the label and pass them to addLabel() 00639 void PieDiagram::addSliceLabel( LabelPaintCache* lpc, const QRectF& drawPosition, uint slice ) 00640 { 00641 const qreal angleLen = d->angleLens[ slice ]; 00642 const qreal startAngle = d->startAngles[ slice ]; 00643 const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked 00644 const qreal sum = valueTotals(); 00645 00646 // Position points are calculated relative to the slice. 00647 // They are calculated as if the slice was 'standing' on its tip and the rim was up, 00648 // so North is the middle (also highest part) of the rim and South is the tip of the slice. 00649 00650 const QPointF south = drawPosition.center(); 00651 const QPointF southEast = south; 00652 const QPointF southWest = south; 00653 const QPointF north = pointOnEllipse( drawPosition, startAngle + angleLen / 2.0 ); 00654 00655 const QPointF northEast = pointOnEllipse( drawPosition, startAngle ); 00656 const QPointF northWest = pointOnEllipse( drawPosition, startAngle + angleLen ); 00657 QPointF center = ( south + north ) / 2.0; 00658 const QPointF east = ( south + northEast ) / 2.0; 00659 const QPointF west = ( south + northWest ) / 2.0; 00660 00661 PositionPoints points( center, northWest, north, northEast, east, southEast, south, southWest, west ); 00662 qreal topAngle = startAngle - 90; 00663 if ( topAngle < 0.0 ) { 00664 topAngle += 360.0; 00665 } 00666 00667 points.setDegrees( KDChartEnums::PositionEast, topAngle ); 00668 points.setDegrees( KDChartEnums::PositionNorthEast, topAngle ); 00669 points.setDegrees( KDChartEnums::PositionWest, topAngle + angleLen ); 00670 points.setDegrees( KDChartEnums::PositionNorthWest, topAngle + angleLen ); 00671 points.setDegrees( KDChartEnums::PositionCenter, topAngle + angleLen / 2.0 ); 00672 points.setDegrees( KDChartEnums::PositionNorth, topAngle + angleLen / 2.0 ); 00673 00674 qreal favoriteTextAngle = 0.0; 00675 if ( autoRotateLabels() ) { 00676 favoriteTextAngle = - ( startAngle + angleLen / 2 ) + 90.0; 00677 while ( favoriteTextAngle <= 0.0 ) { 00678 favoriteTextAngle += 360.0; 00679 } 00680 // flip the label when upside down 00681 if ( favoriteTextAngle > 90.0 && favoriteTextAngle < 270.0 ) { 00682 favoriteTextAngle = favoriteTextAngle - 180.0; 00683 } 00684 // negative angles can have special meaning in addLabel; otherwise they work fine 00685 if ( favoriteTextAngle <= 0.0 ) { 00686 favoriteTextAngle += 360.0; 00687 } 00688 } 00689 00690 d->addLabel( lpc, index, 0, points, Position::Center, Position::Center, 00691 angleLen * sum / 360, favoriteTextAngle ); 00692 } 00693 00694 static bool doSpansOverlap( qreal s1Start, qreal s1End, qreal s2Start, qreal s2End ) 00695 { 00696 if ( s1Start < s2Start ) { 00697 return s1End >= s2Start; 00698 } else { 00699 return s1Start <= s2End; 00700 } 00701 } 00702 00703 static bool doArcsOverlap( qreal a1Start, qreal a1End, qreal a2Start, qreal a2End ) 00704 { 00705 Q_ASSERT( a1Start >= 0 && a1Start <= 360 && a1End >= 0 && a1End <= 360 && 00706 a2Start >= 0 && a2Start <= 360 && a2End >= 0 && a2End <= 360 ); 00707 // all of this could probably be done better... 00708 if ( a1End < a1Start ) { 00709 a1End += 360; 00710 } 00711 if ( a2End < a2Start ) { 00712 a2End += 360; 00713 } 00714 00715 if ( doSpansOverlap( a1Start, a1End, a2Start, a2End ) ) { 00716 return true; 00717 } 00718 if ( a1Start > a2Start ) { 00719 return doSpansOverlap( a1Start - 360.0, a1End - 360.0, a2Start, a2End ); 00720 } else { 00721 return doSpansOverlap( a1Start + 360.0, a1End + 360.0, a2Start, a2End ); 00722 } 00723 } 00724 00732 void PieDiagram::draw3DEffect( QPainter* painter, const QRectF& drawPosition, uint slice ) 00733 { 00734 const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked 00735 const ThreeDPieAttributes threeDAttrs( threeDPieAttributes( index ) ); 00736 if ( ! threeDAttrs.isEnabled() ) { 00737 return; 00738 } 00739 00740 // NOTE: We cannot optimize away drawing some of the effects (even 00741 // when not exploding), because some of the pies might be left out 00742 // in future versions which would make some of the normally hidden 00743 // pies visible. Complex hidden-line algorithms would be much more 00744 // expensive than just drawing for nothing. 00745 00746 // No need to save the brush, will be changed on return from this 00747 // method anyway. 00748 const QBrush brush = this->brush( model()->index( 0, slice, rootIndex() ) ); // checked 00749 if ( threeDAttrs.useShadowColors() ) { 00750 painter->setBrush( QBrush( brush.color().darker() ) ); 00751 } else { 00752 painter->setBrush( brush ); 00753 } 00754 00755 qreal startAngle = d->startAngles[ slice ]; 00756 qreal endAngle = startAngle + d->angleLens[ slice ]; 00757 // Normalize angles 00758 while ( startAngle >= 360 ) 00759 startAngle -= 360; 00760 while ( endAngle >= 360 ) 00761 endAngle -= 360; 00762 Q_ASSERT( startAngle >= 0 && startAngle <= 360 ); 00763 Q_ASSERT( endAngle >= 0 && endAngle <= 360 ); 00764 00765 // positive pie height: absolute value 00766 // negative pie height: relative value 00767 const int depth = threeDAttrs.depth() >= 0.0 ? threeDAttrs.depth() : -threeDAttrs.depth() / 100.0 * drawPosition.height(); 00768 00769 if ( startAngle == endAngle || startAngle == endAngle - 360 ) { // full circle 00770 draw3dOuterRim( painter, drawPosition, depth, 180, 360 ); 00771 } else { 00772 if ( doArcsOverlap( startAngle, endAngle, 180, 360 ) ) { 00773 draw3dOuterRim( painter, drawPosition, depth, startAngle, endAngle ); 00774 } 00775 00776 if ( startAngle >= 270 || startAngle <= 90 ) { 00777 draw3dCutSurface( painter, drawPosition, depth, startAngle ); 00778 } 00779 if ( endAngle >= 90 && endAngle <= 270 ) { 00780 draw3dCutSurface( painter, drawPosition, depth, endAngle ); 00781 } 00782 } 00783 } 00784 00785 00795 void PieDiagram::draw3dCutSurface( QPainter* painter, 00796 const QRectF& rect, 00797 qreal threeDHeight, 00798 qreal angle ) 00799 { 00800 QPolygonF poly( 4 ); 00801 const QPointF center = rect.center(); 00802 const QPointF circlePoint = pointOnEllipse( rect, angle ); 00803 poly[0] = center; 00804 poly[1] = circlePoint; 00805 poly[2] = QPointF( circlePoint.x(), circlePoint.y() + threeDHeight ); 00806 poly[3] = QPointF( center.x(), center.y() + threeDHeight ); 00807 // TODO: add polygon to ReverseMapper 00808 painter->drawPolygon( poly ); 00809 } 00810 00820 void PieDiagram::draw3dOuterRim( QPainter* painter, 00821 const QRectF& rect, 00822 qreal threeDHeight, 00823 qreal startAngle, 00824 qreal endAngle ) 00825 { 00826 // Start with getting the points for the inner arc. 00827 if ( endAngle < startAngle ) { 00828 endAngle += 360; 00829 } 00830 startAngle = qMax( startAngle, qreal( 180.0 ) ); 00831 endAngle = qMin( endAngle, qreal( 360.0 ) ); 00832 00833 int numHalfPoints = trunc( ( endAngle - startAngle ) / granularity() ) + 1; 00834 if ( numHalfPoints < 2 ) { 00835 return; 00836 } 00837 00838 QPolygonF poly( numHalfPoints ); 00839 00840 qreal degree = endAngle; 00841 int iPoint = 0; 00842 bool perfectMatch = false; 00843 while ( degree >= startAngle ) { 00844 poly[ numHalfPoints - iPoint - 1 ] = pointOnEllipse( rect, degree ); 00845 00846 perfectMatch = (degree == startAngle); 00847 degree -= granularity(); 00848 ++iPoint; 00849 } 00850 // if necessary add one more point to fill the last small gap 00851 if ( !perfectMatch ) { 00852 poly.prepend( pointOnEllipse( rect, startAngle ) ); 00853 ++numHalfPoints; 00854 } 00855 00856 poly.resize( numHalfPoints * 2 ); 00857 00858 // Now copy these arcs again into the final array, but in the 00859 // opposite direction and moved down by the 3D height. 00860 for ( int i = numHalfPoints - 1; i >= 0; --i ) { 00861 QPointF pointOnFirstArc( poly[ i ] ); 00862 pointOnFirstArc.setY( pointOnFirstArc.y() + threeDHeight ); 00863 poly[ numHalfPoints * 2 - i - 1 ] = pointOnFirstArc; 00864 } 00865 00866 // TODO: Add polygon to ReverseMapper 00867 painter->drawPolygon( poly ); 00868 } 00869 00876 uint PieDiagram::findSliceAt( qreal angle, int colCount ) 00877 { 00878 for ( int i = 0; i < colCount; ++i ) { 00879 qreal endseg = d->startAngles[ i ] + d->angleLens[ i ]; 00880 if ( d->startAngles[ i ] <= angle && endseg >= angle ) { 00881 return i; 00882 } 00883 } 00884 00885 // If we have not found it, try wrap around 00886 // but only if the current searched angle is < 360 degree 00887 if ( angle < 360 ) 00888 return findSliceAt( angle + 360, colCount ); 00889 // otherwise - what ever went wrong - we return 0 00890 return 0; 00891 } 00892 00893 00900 uint PieDiagram::findLeftSlice( uint slice, int colCount ) 00901 { 00902 if ( slice == 0 ) { 00903 if ( colCount > 1 ) { 00904 return colCount - 1; 00905 } else { 00906 return 0; 00907 } 00908 } else { 00909 return slice - 1; 00910 } 00911 } 00912 00913 00920 uint PieDiagram::findRightSlice( uint slice, int colCount ) 00921 { 00922 int rightSlice = slice + 1; 00923 if ( rightSlice == colCount ) { 00924 rightSlice = 0; 00925 } 00926 return rightSlice; 00927 } 00928 00929 00934 QPointF PieDiagram::pointOnEllipse( const QRectF& boundingBox, qreal angle ) 00935 { 00936 qreal angleRad = DEGTORAD( angle ); 00937 qreal cosAngle = cos( angleRad ); 00938 qreal sinAngle = -sin( angleRad ); 00939 qreal posX = cosAngle * boundingBox.width() / 2.0; 00940 qreal posY = sinAngle * boundingBox.height() / 2.0; 00941 return QPointF( posX + boundingBox.center().x(), 00942 posY + boundingBox.center().y() ); 00943 00944 } 00945 00946 /*virtual*/ 00947 qreal PieDiagram::valueTotals() const 00948 { 00949 if ( !model() ) 00950 return 0; 00951 const int colCount = columnCount(); 00952 qreal total = 0.0; 00953 Q_ASSERT( model()->rowCount() >= 1 ); 00954 for ( int j = 0; j < colCount; ++j ) { 00955 total += qAbs(model()->data( model()->index( 0, j, rootIndex() ) ).toReal()); // checked 00956 } 00957 return total; 00958 } 00959 00960 /*virtual*/ 00961 qreal PieDiagram::numberOfValuesPerDataset() const 00962 { 00963 return model() ? model()->columnCount( rootIndex() ) : 0.0; 00964 } 00965 00966 /*virtual*/ 00967 qreal PieDiagram::numberOfGridRings() const 00968 { 00969 return 1; 00970 }