File indexing completed on 2024-12-15 04:02:34

0001 /*
0002  * SPDX-FileCopyrightText: 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved.
0003  *
0004  * This file is part of the KD Chart library.
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "KChartPieDiagram.h"
0010 #include "KChartPieDiagram_p.h"
0011 
0012 #include "KChartPaintContext.h"
0013 #include "KChartPieAttributes.h"
0014 #include "KChartPolarCoordinatePlane_p.h"
0015 #include "KChartThreeDPieAttributes.h"
0016 #include "KChartPainterSaver_p.h"
0017 #include "KChartMath_p.h"
0018 
0019 #include <QDebug>
0020 #include <QPainter>
0021 #include <QStack>
0022 
0023 
0024 using namespace KChart;
0025 
0026 PieDiagram::Private::Private()
0027   : labelDecorations( PieDiagram::NoDecoration ),
0028     isCollisionAvoidanceEnabled( false )
0029 {
0030 }
0031 
0032 PieDiagram::Private::~Private() {}
0033 
0034 #define d d_func()
0035 
0036 PieDiagram::PieDiagram( QWidget* parent, PolarCoordinatePlane* plane ) :
0037     AbstractPieDiagram( new Private(), parent, plane )
0038 {
0039     init();
0040 }
0041 
0042 PieDiagram::~PieDiagram()
0043 {
0044 }
0045 
0046 void PieDiagram::init()
0047 {
0048 }
0049 
0050 PieDiagram * PieDiagram::clone() const
0051 {
0052     return new PieDiagram( new Private( *d ) );
0053 }
0054 
0055 void PieDiagram::setLabelDecorations( LabelDecorations decorations )
0056 {
0057     d->labelDecorations = decorations;
0058 }
0059 
0060 PieDiagram::LabelDecorations PieDiagram::labelDecorations() const
0061 {
0062     return d->labelDecorations;
0063 }
0064 
0065 void PieDiagram::setLabelCollisionAvoidanceEnabled( bool enabled )
0066 {
0067     d->isCollisionAvoidanceEnabled = enabled;
0068 }
0069 
0070 bool PieDiagram::isLabelCollisionAvoidanceEnabled() const
0071 {
0072     return d->isCollisionAvoidanceEnabled;
0073 }
0074 
0075 const QPair<QPointF, QPointF> PieDiagram::calculateDataBoundaries () const
0076 {
0077     if ( !checkInvariants( true ) || model()->rowCount() < 1 ) return QPair<QPointF, QPointF>( QPointF( 0, 0 ), QPointF( 0, 0 ) );
0078 
0079     const PieAttributes attrs( pieAttributes() );
0080 
0081     QPointF bottomLeft( QPointF( 0, 0 ) );
0082     QPointF topRight;
0083     // If we explode, we need extra space for the slice that has the largest explosion distance.
0084     if ( attrs.explode() ) {
0085         const int colCount = columnCount();
0086         qreal maxExplode = 0.0;
0087         for ( int j = 0; j < colCount; ++j ) {
0088             const PieAttributes columnAttrs( pieAttributes( model()->index( 0, j, rootIndex() ) ) ); // checked
0089             maxExplode = qMax( maxExplode, columnAttrs.explodeFactor() );
0090         }
0091         topRight = QPointF( 1.0 + maxExplode, 1.0 + maxExplode );
0092     } else {
0093         topRight = QPointF( 1.0, 1.0 );
0094     }
0095     return QPair<QPointF, QPointF> ( bottomLeft,  topRight );
0096 }
0097 
0098 
0099 void PieDiagram::paintEvent( QPaintEvent* )
0100 {
0101     QPainter painter ( viewport() );
0102     PaintContext ctx;
0103     ctx.setPainter ( &painter );
0104     ctx.setRectangle( QRectF ( 0, 0, width(), height() ) );
0105     paint ( &ctx );
0106 }
0107 
0108 void PieDiagram::resizeEvent( QResizeEvent* )
0109 {
0110 }
0111 
0112 void PieDiagram::resize( const QSizeF& size )
0113 {
0114     AbstractPieDiagram::resize(size);
0115 }
0116 
0117 void PieDiagram::paint( PaintContext* ctx )
0118 {
0119     // Painting is a two stage process
0120     // In the first stage we figure out how much space is needed
0121     // for text labels.
0122     // In the second stage, we make use of that information and
0123     // perform the actual painting.
0124     placeLabels( ctx );
0125     paintInternal( ctx );
0126 }
0127 
0128 void PieDiagram::calcSliceAngles()
0129 {
0130     // determine slice positions and sizes
0131     const qreal sum = valueTotals();
0132     const qreal sectorsPerValue = 360.0 / sum;
0133     const PolarCoordinatePlane* plane = polarCoordinatePlane();
0134     qreal currentValue = plane ? plane->startPosition() : 0.0;
0135 
0136     const int colCount = columnCount();
0137     d->startAngles.resize( colCount );
0138     d->angleLens.resize( colCount );
0139 
0140     bool atLeastOneValue = false; // guard against completely empty tables
0141     for ( int iColumn = 0; iColumn < colCount; ++iColumn ) {
0142         bool isOk;
0143         const qreal cellValue = qAbs( model()->data( model()->index( 0, iColumn, rootIndex() ) ) // checked
0144             .toReal( &isOk ) );
0145         // toReal() returns 0.0 if there was no value or a non-numeric value
0146         atLeastOneValue = atLeastOneValue || isOk;
0147 
0148         d->startAngles[ iColumn ] = currentValue;
0149         d->angleLens[ iColumn ] = cellValue * sectorsPerValue;
0150 
0151         currentValue = d->startAngles[ iColumn ] + d->angleLens[ iColumn ];
0152     }
0153 
0154     // If there was no value at all, this is the sign for other code to bail out
0155     if ( !atLeastOneValue ) {
0156         d->startAngles.clear();
0157         d->angleLens.clear();
0158     }
0159 }
0160 
0161 void PieDiagram::calcPieSize( const QRectF &contentsRect )
0162 {
0163     d->size = qMin( contentsRect.width(), contentsRect.height() );
0164 
0165     // if any slice explodes, the whole pie needs additional space so we make the basic size smaller
0166     qreal maxExplode = 0.0;
0167     const int colCount = columnCount();
0168     for ( int j = 0; j < colCount; ++j ) {
0169         const PieAttributes columnAttrs( pieAttributes( model()->index( 0, j, rootIndex() ) ) ); // checked
0170         maxExplode = qMax( maxExplode, columnAttrs.explodeFactor() );
0171     }
0172     d->size /= ( 1.0 + 1.0 * maxExplode );
0173 
0174     if ( d->size < 0.0 ) {
0175         d->size = 0;
0176     }
0177 }
0178 
0179 // this is the rect of the top surface of the pie, i.e. excluding the "3D" rim effect.
0180 QRectF PieDiagram::twoDPieRect( const QRectF &contentsRect, const ThreeDPieAttributes& threeDAttrs ) const
0181 {
0182     QRectF pieRect;
0183     if ( !threeDAttrs.isEnabled() ) {
0184         qreal x = ( contentsRect.width() - d->size ) / 2.0;
0185         qreal y = ( contentsRect.height() - d->size ) / 2.0;
0186         pieRect = QRectF( contentsRect.left() + x, contentsRect.top() + y, d->size, d->size );
0187     } else {
0188         // threeD: width is the maximum possible width; height is 1/2 of that
0189         qreal sizeFor3DEffect = 0.0;
0190 
0191         qreal x = ( contentsRect.width() - d->size ) / 2.0;
0192         qreal height = d->size;
0193         // make sure that the height plus the threeDheight is not more than the
0194         // available size
0195         if ( threeDAttrs.depth() >= 0.0 ) {
0196             // positive pie height: absolute value
0197             sizeFor3DEffect = threeDAttrs.depth();
0198             height = d->size - sizeFor3DEffect;
0199         } else {
0200             // negative pie height: relative value
0201             sizeFor3DEffect = - threeDAttrs.depth() / 100.0 * height;
0202             height = d->size - sizeFor3DEffect;
0203         }
0204         qreal y = ( contentsRect.height() - height - sizeFor3DEffect ) / 2.0;
0205 
0206         pieRect = QRectF( contentsRect.left() + x, contentsRect.top() + y, d->size, height );
0207     }
0208     return pieRect;
0209 }
0210 
0211 void PieDiagram::placeLabels( PaintContext* paintContext )
0212 {
0213     if ( !checkInvariants(true) || model()->rowCount() < 1 ) {
0214         return;
0215     }
0216     if ( paintContext->rectangle().isEmpty() || valueTotals() == 0.0 ) {
0217         return;
0218     }
0219 
0220     const ThreeDPieAttributes threeDAttrs( threeDPieAttributes() );
0221     const int colCount = columnCount();
0222 
0223     d->reverseMapper.clear(); // on first call, this sets up the internals of the ReverseMapper.
0224 
0225     calcSliceAngles();
0226     if ( d->startAngles.isEmpty() ) {
0227         return;
0228     }
0229 
0230     calcPieSize( paintContext->rectangle() );
0231 
0232     // keep resizing the pie until the labels and the pie fit into paintContext->rectangle()
0233 
0234     bool tryAgain = true;
0235     while ( tryAgain ) {
0236         tryAgain = false;
0237 
0238         QRectF pieRect = twoDPieRect( paintContext->rectangle(), threeDAttrs );
0239         d->forgetAlreadyPaintedDataValues();
0240         d->labelPaintCache.clear();
0241 
0242         for ( int slice = 0; slice < colCount; slice++ ) {
0243             if ( d->angleLens[ slice ] != 0.0 ) {
0244                 const QRectF explodedPieRect = explodedDrawPosition( pieRect, slice );
0245                 addSliceLabel( &d->labelPaintCache, explodedPieRect, slice );
0246             }
0247         }
0248 
0249         QRectF textBoundingRect;
0250         d->paintDataValueTextsAndMarkers( paintContext, d->labelPaintCache, false, true,
0251                                           &textBoundingRect );
0252         if ( d->isCollisionAvoidanceEnabled ) {
0253             shuffleLabels( &textBoundingRect );
0254         }
0255 
0256         if ( !textBoundingRect.isEmpty() && d->size > 0.0 ) {
0257             const QRectF &clipRect = paintContext->rectangle();
0258             // see by how many pixels the text is clipped on each side
0259             qreal right = qMax( qreal( 0.0 ), textBoundingRect.right() - clipRect.right() );
0260             qreal left = qMax( qreal( 0.0 ), clipRect.left() - textBoundingRect.left() );
0261             // attention here - y coordinates in Qt are inverted compared to the convention in maths
0262             qreal top = qMax( qreal( 0.0 ), clipRect.top() - textBoundingRect.top() );
0263             qreal bottom = qMax( qreal( 0.0 ), textBoundingRect.bottom() - clipRect.bottom() );
0264             qreal maxOverhang = qMax( qMax( right, left ), qMax( top, bottom ) );
0265 
0266             if ( maxOverhang > 0.0 ) {
0267                 // subtract 2x as much because every side only gets half of the total diameter reduction
0268                 // and we have to make up for the overhang on one particular side.
0269                 d->size -= qMin<qreal>( d->size, maxOverhang * 2.0 );
0270                 tryAgain = true;
0271             }
0272         }
0273     }
0274 }
0275 
0276 static int wraparound( int i, int size )
0277 {
0278     while ( i < 0 ) {
0279         i += size;
0280     }
0281     while ( i >= size ) {
0282         i -= size;
0283     }
0284     return i;
0285 }
0286 
0287 //#define SHUFFLE_DEBUG
0288 
0289 void PieDiagram::shuffleLabels( QRectF* textBoundingRect )
0290 {
0291     // things that could be improved here:
0292     // - use a variable number (chosen using angle information) of neighbors to check
0293     // - try harder to arrange the labels to look nice
0294 
0295     // ideas:
0296     // - leave labels that don't collide alone (only if they their offset is zero)
0297     // - use a graphics view for collision detection
0298 
0299     LabelPaintCache& lpc = d->labelPaintCache;
0300     const int n = lpc.paintReplay.size();
0301     bool modified = false;
0302     qreal direction = 5.0;
0303     QVector< qreal > offsets;
0304     offsets.fill( 0.0, n );
0305 
0306     for ( bool lastRoundModified = true; lastRoundModified; ) {
0307         lastRoundModified = false;
0308 
0309         for ( int i = 0; i < n; i++ ) {
0310             const int neighborsToCheck = qMax( 10, lpc.paintReplay.size() - 1 );
0311             const int minComp = wraparound( i - neighborsToCheck / 2, n );
0312             const int maxComp = wraparound( i + ( neighborsToCheck + 1 ) / 2, n );
0313 
0314             QPainterPath& path = lpc.paintReplay[ i ].labelArea;
0315 
0316             for ( int j = minComp; j != maxComp; j = wraparound( j + 1, n ) ) {
0317                 if ( i == j ) {
0318                     continue;
0319                 }
0320                 QPainterPath& otherPath = lpc.paintReplay[ j ].labelArea;
0321 
0322                 while ( ( offsets[ i ] + direction > 0 ) && otherPath.intersects( path ) ) {
0323 #ifdef SHUFFLE_DEBUG
0324                     qDebug() << "collision involving" << j << "and" << i << " -- n =" << n;
0325                     TextAttributes ta = lpc.paintReplay[ i ].attrs.textAttributes();
0326                     ta.setPen( QPen( Qt::white ) );
0327                     lpc.paintReplay[ i ].attrs.setTextAttributes( ta );
0328 #endif
0329                     uint slice = lpc.paintReplay[ i ].index.column();
0330                     qreal angle = DEGTORAD( d->startAngles[ slice ] + d->angleLens[ slice ] / 2.0 );
0331                     qreal dx = cos( angle ) * direction;
0332                     qreal dy = -sin( angle ) * direction;
0333                     offsets[ i ] += direction;
0334                     path.translate( dx, dy );
0335                     lastRoundModified = true;
0336                 }
0337             }
0338         }
0339         direction *= -1.07; // this can "overshoot", but avoids getting trapped in local minimums
0340         modified = modified || lastRoundModified;
0341     }
0342 
0343     if ( modified ) {
0344         for ( int i = 0; i < lpc.paintReplay.size(); i++ ) {
0345             *textBoundingRect |= lpc.paintReplay[ i ].labelArea.boundingRect();
0346         }
0347     }
0348 }
0349 
0350 static QPolygonF polygonFromPainterPath( const QPainterPath &pp )
0351 {
0352     QPolygonF ret;
0353     for ( int i = 0; i < pp.elementCount(); i++ ) {
0354         const QPainterPath::Element& el = pp.elementAt( i );
0355         Q_ASSERT( el.type == QPainterPath::MoveToElement || el.type == QPainterPath::LineToElement );
0356         ret.append( el );
0357     }
0358     return ret;
0359 }
0360 
0361 // you can call it "normalizedProjectionLength" if you like
0362 static qreal normProjection( const QLineF &l1, const QLineF &l2 )
0363 {
0364     const qreal dotProduct = l1.dx() * l2.dx() + l1.dy() * l2.dy();
0365     return qAbs( dotProduct / ( l1.length() * l2.length() ) );
0366 }
0367 
0368 static QLineF labelAttachmentLine( const QPointF &center, const QPointF &start, const QPainterPath &label )
0369 {
0370     Q_ASSERT ( label.elementCount() == 5 );
0371 
0372     // start is assumed to lie on the outer rim of the slice(!), making it possible to derive the
0373     // radius of the pie
0374     const qreal pieRadius = QLineF( center, start ).length();
0375 
0376     // don't draw a line at all when the label is connected to its slice due to at least one of its
0377     // corners falling inside the slice.
0378     for ( int i = 0; i < 4; i++ ) { // point 4 is just a duplicate of point 0
0379         if ( QLineF( label.elementAt( i ), center ).length() < pieRadius ) {
0380             return QLineF();
0381         }
0382     }
0383 
0384     // find the closest edge in the polygon, and its two neighbors
0385     QPointF closeCorners[3];
0386     {
0387         QPointF closest = QPointF( 1000000, 1000000 );
0388         int closestIndex = 0; // better misbehave than crash
0389         for ( int i = 0; i < 4; i++ ) { // point 4 is just a duplicate of point 0
0390             QPointF p = label.elementAt( i );
0391             if ( QLineF( p, center ).length() < QLineF( closest, center ).length() ) {
0392                 closest = p;
0393                 closestIndex = i;
0394             }
0395         }
0396 
0397         closeCorners[ 0 ] = label.elementAt( wraparound( closestIndex - 1, 4 ) );
0398         closeCorners[ 1 ] = closest;
0399         closeCorners[ 2 ] = label.elementAt( wraparound( closestIndex + 1, 4 ) );
0400     }
0401 
0402     QLineF edge1 = QLineF( closeCorners[ 0 ], closeCorners[ 1 ] );
0403     QLineF edge2 = QLineF( closeCorners[ 1 ], closeCorners[ 2 ] );
0404     QLineF connection1 = QLineF( ( closeCorners[ 0 ] + closeCorners[ 1 ] ) / 2.0, center );
0405     QLineF connection2 = QLineF( ( closeCorners[ 1 ] + closeCorners[ 2 ] ) / 2.0, center );
0406     QLineF ret;
0407     // prefer the connecting line meeting its edge at a more perpendicular angle
0408     if ( normProjection( edge1, connection1 ) < normProjection( edge2, connection2 ) ) {
0409         ret = connection1;
0410     } else {
0411         ret = connection2;
0412     }
0413 
0414     // This tends to look a bit better than not doing it *shrug*
0415     ret.setP2( ( start + center ) / 2.0 );
0416 
0417     // make the line end at the rim of the slice (not 100% accurate because the line is not precisely radial)
0418     qreal p1Radius = QLineF( ret.p1(), center ).length();
0419     ret.setLength( p1Radius - pieRadius );
0420 
0421     return ret;
0422 }
0423 
0424 void PieDiagram::paintInternal( PaintContext* paintContext )
0425 {
0426     // note: Not having any data model assigned is no bug
0427     //       but we can not draw a diagram then either.
0428     if ( !checkInvariants( true ) || model()->rowCount() < 1 ) {
0429         return;
0430     }
0431     if ( d->startAngles.isEmpty() || paintContext->rectangle().isEmpty() || valueTotals() == 0.0 ) {
0432         return;
0433     }
0434 
0435     const ThreeDPieAttributes threeDAttrs( threeDPieAttributes() );
0436     const int colCount = columnCount();
0437 
0438     // Paint from back to front ("painter's algorithm") - first draw the backmost slice,
0439     // then the slices on the left and right from back to front, then the frontmost one.
0440 
0441     QRectF pieRect = twoDPieRect( paintContext->rectangle(), threeDAttrs );
0442     const int backmostSlice = findSliceAt( 90, colCount );
0443     const int frontmostSlice = findSliceAt( 270, colCount );
0444     int currentLeftSlice = backmostSlice;
0445     int currentRightSlice = backmostSlice;
0446 
0447     drawSlice( paintContext->painter(), pieRect, backmostSlice );
0448 
0449     if ( backmostSlice == frontmostSlice ) {
0450         const int rightmostSlice = findSliceAt( 0, colCount );
0451         const int leftmostSlice = findSliceAt( 180, colCount );
0452 
0453         if ( backmostSlice == leftmostSlice ) {
0454             currentLeftSlice = findLeftSlice( currentLeftSlice, colCount );
0455         }
0456         if ( backmostSlice == rightmostSlice ) {
0457             currentRightSlice = findRightSlice( currentRightSlice, colCount );
0458         }
0459     }
0460 
0461     while ( currentLeftSlice != frontmostSlice ) {
0462         if ( currentLeftSlice != backmostSlice ) {
0463             drawSlice( paintContext->painter(), pieRect, currentLeftSlice );
0464         }
0465         currentLeftSlice = findLeftSlice( currentLeftSlice, colCount );
0466     }
0467 
0468     while ( currentRightSlice != frontmostSlice ) {
0469         if ( currentRightSlice != backmostSlice ) {
0470             drawSlice( paintContext->painter(), pieRect, currentRightSlice );
0471         }
0472         currentRightSlice = findRightSlice( currentRightSlice, colCount );
0473     }
0474 
0475     // if the backmost slice is not the frontmost slice, we draw the frontmost one last
0476     if ( backmostSlice != frontmostSlice || ! threeDPieAttributes().isEnabled() ) {
0477         drawSlice( paintContext->painter(), pieRect, frontmostSlice );
0478     }
0479 
0480     d->paintDataValueTextsAndMarkers( paintContext, d->labelPaintCache, false, false );
0481     // it's safer to do this at the beginning of placeLabels, but we can save some memory here.
0482     d->forgetAlreadyPaintedDataValues();
0483     // ### maybe move this into AbstractDiagram, also make ReverseMapper deal better with multiple polygons
0484     const QPointF center = paintContext->rectangle().center();
0485     const PainterSaver painterSaver( paintContext->painter() );
0486     paintContext->painter()->setBrush( Qt::NoBrush );
0487     for( const LabelPaintInfo &pi : qAsConst(d->labelPaintCache.paintReplay) ) {
0488         // we expect the PainterPath to be a rectangle
0489         if ( pi.labelArea.elementCount() != 5 ) {
0490             continue;
0491         }
0492 
0493         paintContext->painter()->setPen( pen( pi.index ) );
0494         if ( d->labelDecorations & LineFromSliceDecoration ) {
0495             paintContext->painter()->drawLine( labelAttachmentLine( center, pi.markerPos, pi.labelArea ) );
0496         }
0497         if ( d->labelDecorations & FrameDecoration ) {
0498             paintContext->painter()->drawPath( pi.labelArea );
0499         }
0500         d->reverseMapper.addPolygon( pi.index.row(), pi.index.column(),
0501                                      polygonFromPainterPath( pi.labelArea ) );
0502     }
0503     d->labelPaintCache.clear();
0504     d->startAngles.clear();
0505     d->angleLens.clear();
0506 }
0507 
0508 #if defined ( Q_OS_WIN)
0509 #define trunc(x) ((int)(x))
0510 #endif
0511 
0512 QRectF PieDiagram::explodedDrawPosition( const QRectF& drawPosition, uint slice ) const
0513 {
0514     const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked
0515     const PieAttributes attrs( pieAttributes( index ) );
0516 
0517     QRectF adjustedDrawPosition = drawPosition;
0518     if ( attrs.explode() ) {
0519         qreal startAngle = d->startAngles[ slice ];
0520         qreal angleLen = d->angleLens[ slice ];
0521         qreal explodeAngle = ( DEGTORAD( startAngle + angleLen / 2.0 ) );
0522         qreal explodeDistance = attrs.explodeFactor() * d->size / 2.0;
0523 
0524         adjustedDrawPosition.translate( explodeDistance * cos( explodeAngle ),
0525                                         explodeDistance * - sin( explodeAngle ) );
0526     }
0527     return adjustedDrawPosition;
0528 }
0529 
0530 void PieDiagram::drawSlice( QPainter* painter, const QRectF& drawPosition, uint slice)
0531 {
0532     // Is there anything to draw at all?
0533     if ( d->angleLens[ slice ] == 0.0 ) {
0534         return;
0535     }
0536     const QRectF adjustedDrawPosition = explodedDrawPosition( drawPosition, slice );
0537     draw3DEffect( painter, adjustedDrawPosition, slice );
0538     drawSliceSurface( painter, adjustedDrawPosition, slice );
0539 }
0540 
0541 void PieDiagram::drawSliceSurface( QPainter* painter, const QRectF& drawPosition, uint slice )
0542 {
0543     // Is there anything to draw at all?
0544     const qreal angleLen = d->angleLens[ slice ];
0545     const qreal startAngle = d->startAngles[ slice ];
0546     const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked
0547 
0548     const PieAttributes attrs( pieAttributes( index ) );
0549     const ThreeDPieAttributes threeDAttrs( threeDPieAttributes( index ) );
0550 
0551     painter->setRenderHint ( QPainter::Antialiasing );
0552     QBrush br = brush( index );
0553     if ( threeDAttrs.isEnabled() ) {
0554         br = threeDAttrs.threeDBrush( br, drawPosition );
0555     }
0556     painter->setBrush( br );
0557 
0558     QPen pen = this->pen( index );
0559     if ( threeDAttrs.isEnabled() ) {
0560         pen.setColor( Qt::black );
0561     }
0562     painter->setPen( pen );
0563 
0564     if ( angleLen == 360 ) {
0565         // full circle, avoid nasty line in the middle
0566         painter->drawEllipse( drawPosition );
0567 
0568         //Add polygon to Reverse mapper for showing tool tips.
0569         QPolygonF poly( drawPosition );
0570         d->reverseMapper.addPolygon( index.row(), index.column(), poly );
0571     } else {
0572         // draw the top of this piece
0573         // Start with getting the points for the arc.
0574         const int arcPoints = static_cast<int>(trunc( angleLen / granularity() ));
0575         QPolygonF poly( arcPoints + 2 );
0576         qreal degree = 0.0;
0577         int iPoint = 0;
0578         bool perfectMatch = false;
0579 
0580         while ( degree <= angleLen ) {
0581             poly[ iPoint ] = pointOnEllipse( drawPosition, startAngle + degree );
0582             //qDebug() << degree << angleLen << poly[ iPoint ];
0583             perfectMatch = ( degree == angleLen );
0584             degree += granularity();
0585             ++iPoint;
0586         }
0587         // if necessary add one more point to fill the last small gap
0588         if ( !perfectMatch ) {
0589             poly[ iPoint ] = pointOnEllipse( drawPosition, startAngle + angleLen );
0590 
0591             // add the center point of the piece
0592             poly.append( drawPosition.center() );
0593         } else {
0594             poly[ iPoint ] = drawPosition.center();
0595         }
0596         //find the value and paint it
0597         //fix value position
0598         d->reverseMapper.addPolygon( index.row(), index.column(), poly );
0599 
0600         painter->drawPolygon( poly );
0601     }
0602 }
0603 
0604 // calculate the position points for the label and pass them to addLabel()
0605 void PieDiagram::addSliceLabel( LabelPaintCache* lpc, const QRectF& drawPosition, uint slice )
0606 {
0607     const qreal angleLen = d->angleLens[ slice ];
0608     const qreal startAngle = d->startAngles[ slice ];
0609     const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked
0610     const qreal sum = valueTotals();
0611 
0612     // Position points are calculated relative to the slice.
0613     // They are calculated as if the slice was 'standing' on its tip and the rim was up,
0614     // so North is the middle (also highest part) of the rim and South is the tip of the slice.
0615 
0616     const QPointF south = drawPosition.center();
0617     const QPointF southEast = south;
0618     const QPointF southWest = south;
0619     const QPointF north = pointOnEllipse( drawPosition, startAngle + angleLen / 2.0 );
0620 
0621     const QPointF northEast = pointOnEllipse( drawPosition, startAngle );
0622     const QPointF northWest = pointOnEllipse( drawPosition, startAngle + angleLen );
0623     QPointF center = ( south + north ) / 2.0;
0624     const QPointF east = ( south + northEast ) / 2.0;
0625     const QPointF west = ( south + northWest ) / 2.0;
0626 
0627     PositionPoints points( center, northWest, north, northEast, east, southEast, south, southWest, west );
0628     qreal topAngle = startAngle - 90;
0629     if ( topAngle < 0.0 ) {
0630         topAngle += 360.0;
0631     }
0632 
0633     points.setDegrees( KChartEnums::PositionEast, topAngle );
0634     points.setDegrees( KChartEnums::PositionNorthEast, topAngle );
0635     points.setDegrees( KChartEnums::PositionWest, topAngle + angleLen );
0636     points.setDegrees( KChartEnums::PositionNorthWest, topAngle + angleLen );
0637     points.setDegrees( KChartEnums::PositionCenter, topAngle + angleLen / 2.0 );
0638     points.setDegrees( KChartEnums::PositionNorth, topAngle + angleLen / 2.0 );
0639 
0640     qreal favoriteTextAngle = 0.0;
0641     if ( autoRotateLabels() ) {
0642         favoriteTextAngle = - ( startAngle + angleLen / 2 ) + 90.0;
0643         while ( favoriteTextAngle <= 0.0 ) {
0644             favoriteTextAngle += 360.0;
0645         }
0646         // flip the label when upside down
0647         if ( favoriteTextAngle > 90.0 && favoriteTextAngle < 270.0 ) {
0648             favoriteTextAngle = favoriteTextAngle - 180.0;
0649         }
0650         // negative angles can have special meaning in addLabel; otherwise they work fine
0651         if ( favoriteTextAngle <= 0.0 ) {
0652             favoriteTextAngle += 360.0;
0653         }
0654     }
0655 
0656     d->addLabel( lpc, index, nullptr, points, Position::Center, Position::Center,
0657                  angleLen * sum / 360, favoriteTextAngle );
0658 }
0659 
0660 static bool doSpansOverlap( qreal s1Start, qreal s1End, qreal s2Start, qreal s2End )
0661 {
0662     if ( s1Start < s2Start ) {
0663         return s1End >= s2Start;
0664     } else {
0665         return s1Start <= s2End;
0666     }
0667 }
0668 
0669 static bool doArcsOverlap( qreal a1Start, qreal a1End, qreal a2Start, qreal a2End )
0670 {
0671     Q_ASSERT( a1Start >= 0 && a1Start <= 360 && a1End >= 0 && a1End <= 360 &&
0672               a2Start >= 0 && a2Start <= 360 && a2End >= 0 && a2End <= 360 );
0673     // all of this could probably be done better...
0674     if ( a1End < a1Start ) {
0675         a1End += 360;
0676     }
0677     if ( a2End < a2Start ) {
0678         a2End += 360;
0679     }
0680 
0681     if ( doSpansOverlap( a1Start, a1End, a2Start, a2End ) ) {
0682         return true;
0683     }
0684     if ( a1Start > a2Start ) {
0685         return doSpansOverlap( a1Start - 360.0, a1End - 360.0, a2Start, a2End );
0686     } else {
0687         return doSpansOverlap( a1Start + 360.0, a1End + 360.0, a2Start, a2End );
0688     }
0689 }
0690 
0691 void PieDiagram::draw3DEffect( QPainter* painter, const QRectF& drawPosition, uint slice )
0692 {
0693     const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked
0694     const ThreeDPieAttributes threeDAttrs( threeDPieAttributes( index ) );
0695     if ( ! threeDAttrs.isEnabled() ) {
0696         return;
0697     }
0698 
0699     // NOTE: We cannot optimize away drawing some of the effects (even
0700     // when not exploding), because some of the pies might be left out
0701     // in future versions which would make some of the normally hidden
0702     // pies visible. Complex hidden-line algorithms would be much more
0703     // expensive than just drawing for nothing.
0704 
0705     // No need to save the brush, will be changed on return from this
0706     // method anyway.
0707     const QBrush brush = this->brush( model()->index( 0, slice, rootIndex() ) ); // checked
0708     if ( threeDAttrs.useShadowColors() ) {
0709         painter->setBrush( QBrush( brush.color().darker() ) );
0710     } else {
0711         painter->setBrush( brush );
0712     }
0713 
0714     qreal startAngle = d->startAngles[ slice ];
0715     qreal endAngle = startAngle + d->angleLens[ slice ];
0716     // Normalize angles
0717     while ( startAngle >= 360 )
0718         startAngle -= 360;
0719     while ( endAngle >= 360 )
0720         endAngle -= 360;
0721     Q_ASSERT( startAngle >= 0 && startAngle <= 360 );
0722     Q_ASSERT( endAngle >= 0 && endAngle <= 360 );
0723 
0724     // positive pie height: absolute value
0725     // negative pie height: relative value
0726     const int depth = threeDAttrs.depth() >= 0.0 ? threeDAttrs.depth() : -threeDAttrs.depth() / 100.0 * drawPosition.height();
0727 
0728     if ( startAngle == endAngle || startAngle == endAngle - 360 ) { // full circle
0729         draw3dOuterRim( painter, drawPosition, depth, 180, 360 );
0730     } else {
0731         if ( doArcsOverlap( startAngle, endAngle, 180, 360 ) ) {
0732             draw3dOuterRim( painter, drawPosition, depth, startAngle, endAngle );
0733         }
0734 
0735         if ( startAngle >= 270 || startAngle <= 90 ) {
0736             draw3dCutSurface( painter, drawPosition, depth, startAngle );
0737         }
0738         if ( endAngle >= 90 && endAngle <= 270 ) {
0739             draw3dCutSurface( painter, drawPosition, depth, endAngle );
0740         }
0741     }
0742 }
0743 
0744 
0745 void PieDiagram::draw3dCutSurface( QPainter* painter,
0746         const QRectF& rect,
0747         qreal threeDHeight,
0748         qreal angle )
0749 {
0750     QPolygonF poly( 4 );
0751     const QPointF center = rect.center();
0752     const QPointF circlePoint = pointOnEllipse( rect, angle );
0753     poly[0] = center;
0754     poly[1] = circlePoint;
0755     poly[2] = QPointF( circlePoint.x(), circlePoint.y() + threeDHeight );
0756     poly[3] = QPointF( center.x(), center.y() + threeDHeight );
0757     // TODO: add polygon to ReverseMapper
0758     painter->drawPolygon( poly );
0759 }
0760 
0761 void PieDiagram::draw3dOuterRim( QPainter* painter,
0762         const QRectF& rect,
0763         qreal threeDHeight,
0764         qreal startAngle,
0765         qreal endAngle )
0766 {
0767     // Start with getting the points for the inner arc.
0768     if ( endAngle < startAngle ) {
0769         endAngle += 360;
0770     }
0771     startAngle = qMax( startAngle, qreal( 180.0 ) );
0772     endAngle = qMin( endAngle, qreal( 360.0 ) );
0773 
0774     int numHalfPoints = trunc( ( endAngle - startAngle ) / granularity() ) + 1;
0775     if ( numHalfPoints < 2 ) {
0776         return;
0777     }
0778 
0779     QPolygonF poly( numHalfPoints );
0780 
0781     qreal degree = endAngle;
0782     int iPoint = 0;
0783     bool perfectMatch = false;
0784     while ( degree >= startAngle ) {
0785         poly[ numHalfPoints - iPoint - 1 ] = pointOnEllipse( rect, degree );
0786 
0787         perfectMatch = (degree == startAngle);
0788         degree -= granularity();
0789         ++iPoint;
0790     }
0791     // if necessary add one more point to fill the last small gap
0792     if ( !perfectMatch ) {
0793         poly.prepend( pointOnEllipse( rect, startAngle ) );
0794         ++numHalfPoints;
0795     }
0796 
0797     poly.resize( numHalfPoints * 2 );
0798 
0799     // Now copy these arcs again into the final array, but in the
0800     // opposite direction and moved down by the 3D height.
0801     for ( int i = numHalfPoints - 1; i >= 0; --i ) {
0802         QPointF pointOnFirstArc( poly[ i ] );
0803         pointOnFirstArc.setY( pointOnFirstArc.y() + threeDHeight );
0804         poly[ numHalfPoints * 2 - i - 1 ] = pointOnFirstArc;
0805     }
0806 
0807     // TODO: Add polygon to ReverseMapper
0808     painter->drawPolygon( poly );
0809 }
0810 
0811 uint PieDiagram::findSliceAt( qreal angle, int colCount )
0812 {
0813     for ( int i = 0; i < colCount; ++i ) {
0814         qreal endseg = d->startAngles[ i ] + d->angleLens[ i ];
0815         if ( d->startAngles[ i ] <= angle &&  endseg >= angle ) {
0816             return i;
0817         }
0818     }
0819 
0820     // If we have not found it, try wrap around
0821     // but only if the current searched angle is < 360 degree
0822     if ( angle < 360 )
0823         return findSliceAt( angle + 360, colCount );
0824     // otherwise - what ever went wrong - we return 0
0825     return 0;
0826 }
0827 
0828 
0829 uint PieDiagram::findLeftSlice( uint slice, int colCount )
0830 {
0831     if ( slice == 0 ) {
0832         if ( colCount > 1 ) {
0833             return colCount - 1;
0834         } else {
0835             return 0;
0836         }
0837     } else {
0838         return slice - 1;
0839     }
0840 }
0841 
0842 
0843 uint PieDiagram::findRightSlice( uint slice, int colCount )
0844 {
0845     int rightSlice = slice + 1;
0846     if ( rightSlice == colCount ) {
0847         rightSlice = 0;
0848     }
0849     return rightSlice;
0850 }
0851 
0852 
0853 QPointF PieDiagram::pointOnEllipse( const QRectF& boundingBox, qreal angle )
0854 {
0855     qreal angleRad = DEGTORAD( angle );
0856     qreal cosAngle = cos( angleRad );
0857     qreal sinAngle = -sin( angleRad );
0858     qreal posX = cosAngle * boundingBox.width() / 2.0;
0859     qreal posY = sinAngle * boundingBox.height() / 2.0;
0860     return QPointF( posX + boundingBox.center().x(),
0861                     posY + boundingBox.center().y() );
0862 
0863 }
0864 
0865 /*virtual*/
0866 qreal PieDiagram::valueTotals() const
0867 {
0868     if ( !model() )
0869         return 0;
0870     const int colCount = columnCount();
0871     qreal total = 0.0;
0872     // non-empty models need a row with data
0873     Q_ASSERT( colCount == 0 || model()->rowCount() >= 1 );
0874     for ( int j = 0; j < colCount; ++j ) {
0875       total += qAbs(model()->data( model()->index( 0, j, rootIndex() ) ).toReal()); // checked
0876     }
0877     return total;
0878 }
0879 
0880 /*virtual*/
0881 qreal PieDiagram::numberOfValuesPerDataset() const
0882 {
0883     return model() ? model()->columnCount( rootIndex() ) : 0.0;
0884 }
0885 
0886 /*virtual*/
0887 qreal PieDiagram::numberOfGridRings() const
0888 {
0889     return 1;
0890 }