KD Chart 2  [rev.2.5]
KDChartPieDiagram.cpp
Go to the documentation of this file.
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 &center, 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 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Defines

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