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 ¢er, 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 }