1 /*
2  * Copyright (C) 2001-2015 Klaralvdalens Datakonsult AB.  All rights reserved.
3  *
4  * This file is part of the KD Chart library.
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of
9  * the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
18  */
19 
20 #include "KChartPieDiagram.h"
21 #include "KChartPieDiagram_p.h"
22 
23 #include "KChartPaintContext.h"
24 #include "KChartPieAttributes.h"
25 #include "KChartPolarCoordinatePlane_p.h"
26 #include "KChartThreeDPieAttributes.h"
27 #include "KChartPainterSaver_p.h"
28 #include "KChartMath_p.h"
29 
30 #include <QDebug>
31 #include <QPainter>
32 #include <QStack>
33 
34 
35 using namespace KChart;
36 
Private()37 PieDiagram::Private::Private()
38   : labelDecorations( PieDiagram::NoDecoration ),
39     isCollisionAvoidanceEnabled( false )
40 {
41 }
42 
~Private()43 PieDiagram::Private::~Private() {}
44 
45 #define d d_func()
46 
PieDiagram(QWidget * parent,PolarCoordinatePlane * plane)47 PieDiagram::PieDiagram( QWidget* parent, PolarCoordinatePlane* plane ) :
48     AbstractPieDiagram( new Private(), parent, plane )
49 {
50     init();
51 }
52 
~PieDiagram()53 PieDiagram::~PieDiagram()
54 {
55 }
56 
init()57 void PieDiagram::init()
58 {
59 }
60 
clone() const61 PieDiagram * PieDiagram::clone() const
62 {
63     return new PieDiagram( new Private( *d ) );
64 }
65 
setLabelDecorations(LabelDecorations decorations)66 void PieDiagram::setLabelDecorations( LabelDecorations decorations )
67 {
68     d->labelDecorations = decorations;
69 }
70 
labelDecorations() const71 PieDiagram::LabelDecorations PieDiagram::labelDecorations() const
72 {
73     return d->labelDecorations;
74 }
75 
setLabelCollisionAvoidanceEnabled(bool enabled)76 void PieDiagram::setLabelCollisionAvoidanceEnabled( bool enabled )
77 {
78     d->isCollisionAvoidanceEnabled = enabled;
79 }
80 
isLabelCollisionAvoidanceEnabled() const81 bool PieDiagram::isLabelCollisionAvoidanceEnabled() const
82 {
83     return d->isCollisionAvoidanceEnabled;
84 }
85 
calculateDataBoundaries() const86 const QPair<QPointF, QPointF> PieDiagram::calculateDataBoundaries () const
87 {
88     if ( !checkInvariants( true ) || model()->rowCount() < 1 ) return QPair<QPointF, QPointF>( QPointF( 0, 0 ), QPointF( 0, 0 ) );
89 
90     const PieAttributes attrs( pieAttributes() );
91 
92     QPointF bottomLeft( QPointF( 0, 0 ) );
93     QPointF topRight;
94     // If we explode, we need extra space for the slice that has the largest explosion distance.
95     if ( attrs.explode() ) {
96         const int colCount = columnCount();
97         qreal maxExplode = 0.0;
98         for ( int j = 0; j < colCount; ++j ) {
99             const PieAttributes columnAttrs( pieAttributes( model()->index( 0, j, rootIndex() ) ) ); // checked
100             maxExplode = qMax( maxExplode, columnAttrs.explodeFactor() );
101         }
102         topRight = QPointF( 1.0 + maxExplode, 1.0 + maxExplode );
103     } else {
104         topRight = QPointF( 1.0, 1.0 );
105     }
106     return QPair<QPointF, QPointF> ( bottomLeft,  topRight );
107 }
108 
109 
paintEvent(QPaintEvent *)110 void PieDiagram::paintEvent( QPaintEvent* )
111 {
112     QPainter painter ( viewport() );
113     PaintContext ctx;
114     ctx.setPainter ( &painter );
115     ctx.setRectangle( QRectF ( 0, 0, width(), height() ) );
116     paint ( &ctx );
117 }
118 
resizeEvent(QResizeEvent *)119 void PieDiagram::resizeEvent( QResizeEvent* )
120 {
121 }
122 
resize(const QSizeF & size)123 void PieDiagram::resize( const QSizeF& size )
124 {
125     AbstractPieDiagram::resize(size);
126 }
127 
paint(PaintContext * ctx)128 void PieDiagram::paint( PaintContext* ctx )
129 {
130     // Painting is a two stage process
131     // In the first stage we figure out how much space is needed
132     // for text labels.
133     // In the second stage, we make use of that information and
134     // perform the actual painting.
135     placeLabels( ctx );
136     paintInternal( ctx );
137 }
138 
calcSliceAngles()139 void PieDiagram::calcSliceAngles()
140 {
141     // determine slice positions and sizes
142     const qreal sum = valueTotals();
143     const qreal sectorsPerValue = 360.0 / sum;
144     const PolarCoordinatePlane* plane = polarCoordinatePlane();
145     qreal currentValue = plane ? plane->startPosition() : 0.0;
146 
147     const int colCount = columnCount();
148     d->startAngles.resize( colCount );
149     d->angleLens.resize( colCount );
150 
151     bool atLeastOneValue = false; // guard against completely empty tables
152     for ( int iColumn = 0; iColumn < colCount; ++iColumn ) {
153         bool isOk;
154         const qreal cellValue = qAbs( model()->data( model()->index( 0, iColumn, rootIndex() ) ) // checked
155             .toReal( &isOk ) );
156         // toReal() returns 0.0 if there was no value or a non-numeric value
157         atLeastOneValue = atLeastOneValue || isOk;
158 
159         d->startAngles[ iColumn ] = currentValue;
160         d->angleLens[ iColumn ] = cellValue * sectorsPerValue;
161 
162         currentValue = d->startAngles[ iColumn ] + d->angleLens[ iColumn ];
163     }
164 
165     // If there was no value at all, this is the sign for other code to bail out
166     if ( !atLeastOneValue ) {
167         d->startAngles.clear();
168         d->angleLens.clear();
169     }
170 }
171 
calcPieSize(const QRectF & contentsRect)172 void PieDiagram::calcPieSize( const QRectF &contentsRect )
173 {
174     d->size = qMin( contentsRect.width(), contentsRect.height() );
175 
176     // if any slice explodes, the whole pie needs additional space so we make the basic size smaller
177     qreal maxExplode = 0.0;
178     const int colCount = columnCount();
179     for ( int j = 0; j < colCount; ++j ) {
180         const PieAttributes columnAttrs( pieAttributes( model()->index( 0, j, rootIndex() ) ) ); // checked
181         maxExplode = qMax( maxExplode, columnAttrs.explodeFactor() );
182     }
183     d->size /= ( 1.0 + 1.0 * maxExplode );
184 
185     if ( d->size < 0.0 ) {
186         d->size = 0;
187     }
188 }
189 
190 // this is the rect of the top surface of the pie, i.e. excluding the "3D" rim effect.
twoDPieRect(const QRectF & contentsRect,const ThreeDPieAttributes & threeDAttrs) const191 QRectF PieDiagram::twoDPieRect( const QRectF &contentsRect, const ThreeDPieAttributes& threeDAttrs ) const
192 {
193     QRectF pieRect;
194     if ( !threeDAttrs.isEnabled() ) {
195         qreal x = ( contentsRect.width() - d->size ) / 2.0;
196         qreal y = ( contentsRect.height() - d->size ) / 2.0;
197         pieRect = QRectF( contentsRect.left() + x, contentsRect.top() + y, d->size, d->size );
198     } else {
199         // threeD: width is the maximum possible width; height is 1/2 of that
200         qreal sizeFor3DEffect = 0.0;
201 
202         qreal x = ( contentsRect.width() - d->size ) / 2.0;
203         qreal height = d->size;
204         // make sure that the height plus the threeDheight is not more than the
205         // available size
206         if ( threeDAttrs.depth() >= 0.0 ) {
207             // positive pie height: absolute value
208             sizeFor3DEffect = threeDAttrs.depth();
209             height = d->size - sizeFor3DEffect;
210         } else {
211             // negative pie height: relative value
212             sizeFor3DEffect = - threeDAttrs.depth() / 100.0 * height;
213             height = d->size - sizeFor3DEffect;
214         }
215         qreal y = ( contentsRect.height() - height - sizeFor3DEffect ) / 2.0;
216 
217         pieRect = QRectF( contentsRect.left() + x, contentsRect.top() + y, d->size, height );
218     }
219     return pieRect;
220 }
221 
placeLabels(PaintContext * paintContext)222 void PieDiagram::placeLabels( PaintContext* paintContext )
223 {
224     if ( !checkInvariants(true) || model()->rowCount() < 1 ) {
225         return;
226     }
227     if ( paintContext->rectangle().isEmpty() || valueTotals() == 0.0 ) {
228         return;
229     }
230 
231     const ThreeDPieAttributes threeDAttrs( threeDPieAttributes() );
232     const int colCount = columnCount();
233 
234     d->reverseMapper.clear(); // on first call, this sets up the internals of the ReverseMapper.
235 
236     calcSliceAngles();
237     if ( d->startAngles.isEmpty() ) {
238         return;
239     }
240 
241     calcPieSize( paintContext->rectangle() );
242 
243     // keep resizing the pie until the labels and the pie fit into paintContext->rectangle()
244 
245     bool tryAgain = true;
246     while ( tryAgain ) {
247         tryAgain = false;
248 
249         QRectF pieRect = twoDPieRect( paintContext->rectangle(), threeDAttrs );
250         d->forgetAlreadyPaintedDataValues();
251         d->labelPaintCache.clear();
252 
253         for ( int slice = 0; slice < colCount; slice++ ) {
254             if ( d->angleLens[ slice ] != 0.0 ) {
255                 const QRectF explodedPieRect = explodedDrawPosition( pieRect, slice );
256                 addSliceLabel( &d->labelPaintCache, explodedPieRect, slice );
257             }
258         }
259 
260         QRectF textBoundingRect;
261         d->paintDataValueTextsAndMarkers( paintContext, d->labelPaintCache, false, true,
262                                           &textBoundingRect );
263         if ( d->isCollisionAvoidanceEnabled ) {
264             shuffleLabels( &textBoundingRect );
265         }
266 
267         if ( !textBoundingRect.isEmpty() && d->size > 0.0 ) {
268             const QRectF &clipRect = paintContext->rectangle();
269             // see by how many pixels the text is clipped on each side
270             qreal right = qMax( qreal( 0.0 ), textBoundingRect.right() - clipRect.right() );
271             qreal left = qMax( qreal( 0.0 ), clipRect.left() - textBoundingRect.left() );
272             // attention here - y coordinates in Qt are inverted compared to the convention in maths
273             qreal top = qMax( qreal( 0.0 ), clipRect.top() - textBoundingRect.top() );
274             qreal bottom = qMax( qreal( 0.0 ), textBoundingRect.bottom() - clipRect.bottom() );
275             qreal maxOverhang = qMax( qMax( right, left ), qMax( top, bottom ) );
276 
277             if ( maxOverhang > 0.0 ) {
278                 // subtract 2x as much because every side only gets half of the total diameter reduction
279                 // and we have to make up for the overhang on one particular side.
280                 d->size -= qMin<qreal>( d->size, maxOverhang * 2.0 );
281                 tryAgain = true;
282             }
283         }
284     }
285 }
286 
wraparound(int i,int size)287 static int wraparound( int i, int size )
288 {
289     while ( i < 0 ) {
290         i += size;
291     }
292     while ( i >= size ) {
293         i -= size;
294     }
295     return i;
296 }
297 
298 //#define SHUFFLE_DEBUG
299 
shuffleLabels(QRectF * textBoundingRect)300 void PieDiagram::shuffleLabels( QRectF* textBoundingRect )
301 {
302     // things that could be improved here:
303     // - use a variable number (chosen using angle information) of neighbors to check
304     // - try harder to arrange the labels to look nice
305 
306     // ideas:
307     // - leave labels that don't collide alone (only if they their offset is zero)
308     // - use a graphics view for collision detection
309 
310     LabelPaintCache& lpc = d->labelPaintCache;
311     const int n = lpc.paintReplay.size();
312     bool modified = false;
313     qreal direction = 5.0;
314     QVector< qreal > offsets;
315     offsets.fill( 0.0, n );
316 
317     for ( bool lastRoundModified = true; lastRoundModified; ) {
318         lastRoundModified = false;
319 
320         for ( int i = 0; i < n; i++ ) {
321             const int neighborsToCheck = qMax( 10, lpc.paintReplay.size() - 1 );
322             const int minComp = wraparound( i - neighborsToCheck / 2, n );
323             const int maxComp = wraparound( i + ( neighborsToCheck + 1 ) / 2, n );
324 
325             QPainterPath& path = lpc.paintReplay[ i ].labelArea;
326 
327             for ( int j = minComp; j != maxComp; j = wraparound( j + 1, n ) ) {
328                 if ( i == j ) {
329                     continue;
330                 }
331                 QPainterPath& otherPath = lpc.paintReplay[ j ].labelArea;
332 
333                 while ( ( offsets[ i ] + direction > 0 ) && otherPath.intersects( path ) ) {
334 #ifdef SHUFFLE_DEBUG
335                     qDebug() << "collision involving" << j << "and" << i << " -- n =" << n;
336                     TextAttributes ta = lpc.paintReplay[ i ].attrs.textAttributes();
337                     ta.setPen( QPen( Qt::white ) );
338                     lpc.paintReplay[ i ].attrs.setTextAttributes( ta );
339 #endif
340                     uint slice = lpc.paintReplay[ i ].index.column();
341                     qreal angle = DEGTORAD( d->startAngles[ slice ] + d->angleLens[ slice ] / 2.0 );
342                     qreal dx = cos( angle ) * direction;
343                     qreal dy = -sin( angle ) * direction;
344                     offsets[ i ] += direction;
345                     path.translate( dx, dy );
346                     lastRoundModified = true;
347                 }
348             }
349         }
350         direction *= -1.07; // this can "overshoot", but avoids getting trapped in local minimums
351         modified = modified || lastRoundModified;
352     }
353 
354     if ( modified ) {
355         for ( int i = 0; i < lpc.paintReplay.size(); i++ ) {
356             *textBoundingRect |= lpc.paintReplay[ i ].labelArea.boundingRect();
357         }
358     }
359 }
360 
polygonFromPainterPath(const QPainterPath & pp)361 static QPolygonF polygonFromPainterPath( const QPainterPath &pp )
362 {
363     QPolygonF ret;
364     for ( int i = 0; i < pp.elementCount(); i++ ) {
365         const QPainterPath::Element& el = pp.elementAt( i );
366         Q_ASSERT( el.type == QPainterPath::MoveToElement || el.type == QPainterPath::LineToElement );
367         ret.append( el );
368     }
369     return ret;
370 }
371 
372 // you can call it "normalizedProjectionLength" if you like
normProjection(const QLineF & l1,const QLineF & l2)373 static qreal normProjection( const QLineF &l1, const QLineF &l2 )
374 {
375     const qreal dotProduct = l1.dx() * l2.dx() + l1.dy() * l2.dy();
376     return qAbs( dotProduct / ( l1.length() * l2.length() ) );
377 }
378 
labelAttachmentLine(const QPointF & center,const QPointF & start,const QPainterPath & label)379 static QLineF labelAttachmentLine( const QPointF &center, const QPointF &start, const QPainterPath &label )
380 {
381     Q_ASSERT ( label.elementCount() == 5 );
382 
383     // start is assumed to lie on the outer rim of the slice(!), making it possible to derive the
384     // radius of the pie
385     const qreal pieRadius = QLineF( center, start ).length();
386 
387     // don't draw a line at all when the label is connected to its slice due to at least one of its
388     // corners falling inside the slice.
389     for ( int i = 0; i < 4; i++ ) { // point 4 is just a duplicate of point 0
390         if ( QLineF( label.elementAt( i ), center ).length() < pieRadius ) {
391             return QLineF();
392         }
393     }
394 
395     // find the closest edge in the polygon, and its two neighbors
396     QPointF closeCorners[3];
397     {
398         QPointF closest = QPointF( 1000000, 1000000 );
399         int closestIndex = 0; // better misbehave than crash
400         for ( int i = 0; i < 4; i++ ) { // point 4 is just a duplicate of point 0
401             QPointF p = label.elementAt( i );
402             if ( QLineF( p, center ).length() < QLineF( closest, center ).length() ) {
403                 closest = p;
404                 closestIndex = i;
405             }
406         }
407 
408         closeCorners[ 0 ] = label.elementAt( wraparound( closestIndex - 1, 4 ) );
409         closeCorners[ 1 ] = closest;
410         closeCorners[ 2 ] = label.elementAt( wraparound( closestIndex + 1, 4 ) );
411     }
412 
413     QLineF edge1 = QLineF( closeCorners[ 0 ], closeCorners[ 1 ] );
414     QLineF edge2 = QLineF( closeCorners[ 1 ], closeCorners[ 2 ] );
415     QLineF connection1 = QLineF( ( closeCorners[ 0 ] + closeCorners[ 1 ] ) / 2.0, center );
416     QLineF connection2 = QLineF( ( closeCorners[ 1 ] + closeCorners[ 2 ] ) / 2.0, center );
417     QLineF ret;
418     // prefer the connecting line meeting its edge at a more perpendicular angle
419     if ( normProjection( edge1, connection1 ) < normProjection( edge2, connection2 ) ) {
420         ret = connection1;
421     } else {
422         ret = connection2;
423     }
424 
425     // This tends to look a bit better than not doing it *shrug*
426     ret.setP2( ( start + center ) / 2.0 );
427 
428     // make the line end at the rim of the slice (not 100% accurate because the line is not precisely radial)
429     qreal p1Radius = QLineF( ret.p1(), center ).length();
430     ret.setLength( p1Radius - pieRadius );
431 
432     return ret;
433 }
434 
paintInternal(PaintContext * paintContext)435 void PieDiagram::paintInternal( PaintContext* paintContext )
436 {
437     // note: Not having any data model assigned is no bug
438     //       but we can not draw a diagram then either.
439     if ( !checkInvariants( true ) || model()->rowCount() < 1 ) {
440         return;
441     }
442     if ( d->startAngles.isEmpty() || paintContext->rectangle().isEmpty() || valueTotals() == 0.0 ) {
443         return;
444     }
445 
446     const ThreeDPieAttributes threeDAttrs( threeDPieAttributes() );
447     const int colCount = columnCount();
448 
449     // Paint from back to front ("painter's algorithm") - first draw the backmost slice,
450     // then the slices on the left and right from back to front, then the frontmost one.
451 
452     QRectF pieRect = twoDPieRect( paintContext->rectangle(), threeDAttrs );
453     const int backmostSlice = findSliceAt( 90, colCount );
454     const int frontmostSlice = findSliceAt( 270, colCount );
455     int currentLeftSlice = backmostSlice;
456     int currentRightSlice = backmostSlice;
457 
458     drawSlice( paintContext->painter(), pieRect, backmostSlice );
459 
460     if ( backmostSlice == frontmostSlice ) {
461         const int rightmostSlice = findSliceAt( 0, colCount );
462         const int leftmostSlice = findSliceAt( 180, colCount );
463 
464         if ( backmostSlice == leftmostSlice ) {
465             currentLeftSlice = findLeftSlice( currentLeftSlice, colCount );
466         }
467         if ( backmostSlice == rightmostSlice ) {
468             currentRightSlice = findRightSlice( currentRightSlice, colCount );
469         }
470     }
471 
472     while ( currentLeftSlice != frontmostSlice ) {
473         if ( currentLeftSlice != backmostSlice ) {
474             drawSlice( paintContext->painter(), pieRect, currentLeftSlice );
475         }
476         currentLeftSlice = findLeftSlice( currentLeftSlice, colCount );
477     }
478 
479     while ( currentRightSlice != frontmostSlice ) {
480         if ( currentRightSlice != backmostSlice ) {
481             drawSlice( paintContext->painter(), pieRect, currentRightSlice );
482         }
483         currentRightSlice = findRightSlice( currentRightSlice, colCount );
484     }
485 
486     // if the backmost slice is not the frontmost slice, we draw the frontmost one last
487     if ( backmostSlice != frontmostSlice || ! threeDPieAttributes().isEnabled() ) {
488         drawSlice( paintContext->painter(), pieRect, frontmostSlice );
489     }
490 
491     d->paintDataValueTextsAndMarkers( paintContext, d->labelPaintCache, false, false );
492     // it's safer to do this at the beginning of placeLabels, but we can save some memory here.
493     d->forgetAlreadyPaintedDataValues();
494     // ### maybe move this into AbstractDiagram, also make ReverseMapper deal better with multiple polygons
495     const QPointF center = paintContext->rectangle().center();
496     const PainterSaver painterSaver( paintContext->painter() );
497     paintContext->painter()->setBrush( Qt::NoBrush );
498     Q_FOREACH( const LabelPaintInfo &pi, d->labelPaintCache.paintReplay ) {
499         // we expect the PainterPath to be a rectangle
500         if ( pi.labelArea.elementCount() != 5 ) {
501             continue;
502         }
503 
504         paintContext->painter()->setPen( pen( pi.index ) );
505         if ( d->labelDecorations & LineFromSliceDecoration ) {
506             paintContext->painter()->drawLine( labelAttachmentLine( center, pi.markerPos, pi.labelArea ) );
507         }
508         if ( d->labelDecorations & FrameDecoration ) {
509             paintContext->painter()->drawPath( pi.labelArea );
510         }
511         d->reverseMapper.addPolygon( pi.index.row(), pi.index.column(),
512                                      polygonFromPainterPath( pi.labelArea ) );
513     }
514     d->labelPaintCache.clear();
515     d->startAngles.clear();
516     d->angleLens.clear();
517 }
518 
519 #if defined ( Q_OS_WIN)
520 #define trunc(x) ((int)(x))
521 #endif
522 
explodedDrawPosition(const QRectF & drawPosition,uint slice) const523 QRectF PieDiagram::explodedDrawPosition( const QRectF& drawPosition, uint slice ) const
524 {
525     const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked
526     const PieAttributes attrs( pieAttributes( index ) );
527 
528     QRectF adjustedDrawPosition = drawPosition;
529     if ( attrs.explode() ) {
530         qreal startAngle = d->startAngles[ slice ];
531         qreal angleLen = d->angleLens[ slice ];
532         qreal explodeAngle = ( DEGTORAD( startAngle + angleLen / 2.0 ) );
533         qreal explodeDistance = attrs.explodeFactor() * d->size / 2.0;
534 
535         adjustedDrawPosition.translate( explodeDistance * cos( explodeAngle ),
536                                         explodeDistance * - sin( explodeAngle ) );
537     }
538     return adjustedDrawPosition;
539 }
540 
drawSlice(QPainter * painter,const QRectF & drawPosition,uint slice)541 void PieDiagram::drawSlice( QPainter* painter, const QRectF& drawPosition, uint slice)
542 {
543     // Is there anything to draw at all?
544     if ( d->angleLens[ slice ] == 0.0 ) {
545         return;
546     }
547     const QRectF adjustedDrawPosition = explodedDrawPosition( drawPosition, slice );
548     draw3DEffect( painter, adjustedDrawPosition, slice );
549     drawSliceSurface( painter, adjustedDrawPosition, slice );
550 }
551 
drawSliceSurface(QPainter * painter,const QRectF & drawPosition,uint slice)552 void PieDiagram::drawSliceSurface( QPainter* painter, const QRectF& drawPosition, uint slice )
553 {
554     // Is there anything to draw at all?
555     const qreal angleLen = d->angleLens[ slice ];
556     const qreal startAngle = d->startAngles[ slice ];
557     const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked
558 
559     const PieAttributes attrs( pieAttributes( index ) );
560     const ThreeDPieAttributes threeDAttrs( threeDPieAttributes( index ) );
561 
562     painter->setRenderHint ( QPainter::Antialiasing );
563     QBrush br = brush( index );
564     if ( threeDAttrs.isEnabled() ) {
565         br = threeDAttrs.threeDBrush( br, drawPosition );
566     }
567     painter->setBrush( br );
568 
569     QPen pen = this->pen( index );
570     if ( threeDAttrs.isEnabled() ) {
571         pen.setColor( Qt::black );
572     }
573     painter->setPen( pen );
574 
575     if ( angleLen == 360 ) {
576         // full circle, avoid nasty line in the middle
577         painter->drawEllipse( drawPosition );
578 
579         //Add polygon to Reverse mapper for showing tool tips.
580         QPolygonF poly( drawPosition );
581         d->reverseMapper.addPolygon( index.row(), index.column(), poly );
582     } else {
583         // draw the top of this piece
584         // Start with getting the points for the arc.
585         const int arcPoints = static_cast<int>(trunc( angleLen / granularity() ));
586         QPolygonF poly( arcPoints + 2 );
587         qreal degree = 0.0;
588         int iPoint = 0;
589         bool perfectMatch = false;
590 
591         while ( degree <= angleLen ) {
592             poly[ iPoint ] = pointOnEllipse( drawPosition, startAngle + degree );
593             //qDebug() << degree << angleLen << poly[ iPoint ];
594             perfectMatch = ( degree == angleLen );
595             degree += granularity();
596             ++iPoint;
597         }
598         // if necessary add one more point to fill the last small gap
599         if ( !perfectMatch ) {
600             poly[ iPoint ] = pointOnEllipse( drawPosition, startAngle + angleLen );
601 
602             // add the center point of the piece
603             poly.append( drawPosition.center() );
604         } else {
605             poly[ iPoint ] = drawPosition.center();
606         }
607         //find the value and paint it
608         //fix value position
609         d->reverseMapper.addPolygon( index.row(), index.column(), poly );
610 
611         painter->drawPolygon( poly );
612     }
613 }
614 
615 // calculate the position points for the label and pass them to addLabel()
addSliceLabel(LabelPaintCache * lpc,const QRectF & drawPosition,uint slice)616 void PieDiagram::addSliceLabel( LabelPaintCache* lpc, const QRectF& drawPosition, uint slice )
617 {
618     const qreal angleLen = d->angleLens[ slice ];
619     const qreal startAngle = d->startAngles[ slice ];
620     const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked
621     const qreal sum = valueTotals();
622 
623     // Position points are calculated relative to the slice.
624     // They are calculated as if the slice was 'standing' on its tip and the rim was up,
625     // so North is the middle (also highest part) of the rim and South is the tip of the slice.
626 
627     const QPointF south = drawPosition.center();
628     const QPointF southEast = south;
629     const QPointF southWest = south;
630     const QPointF north = pointOnEllipse( drawPosition, startAngle + angleLen / 2.0 );
631 
632     const QPointF northEast = pointOnEllipse( drawPosition, startAngle );
633     const QPointF northWest = pointOnEllipse( drawPosition, startAngle + angleLen );
634     QPointF center = ( south + north ) / 2.0;
635     const QPointF east = ( south + northEast ) / 2.0;
636     const QPointF west = ( south + northWest ) / 2.0;
637 
638     PositionPoints points( center, northWest, north, northEast, east, southEast, south, southWest, west );
639     qreal topAngle = startAngle - 90;
640     if ( topAngle < 0.0 ) {
641         topAngle += 360.0;
642     }
643 
644     points.setDegrees( KChartEnums::PositionEast, topAngle );
645     points.setDegrees( KChartEnums::PositionNorthEast, topAngle );
646     points.setDegrees( KChartEnums::PositionWest, topAngle + angleLen );
647     points.setDegrees( KChartEnums::PositionNorthWest, topAngle + angleLen );
648     points.setDegrees( KChartEnums::PositionCenter, topAngle + angleLen / 2.0 );
649     points.setDegrees( KChartEnums::PositionNorth, topAngle + angleLen / 2.0 );
650 
651     qreal favoriteTextAngle = 0.0;
652     if ( autoRotateLabels() ) {
653         favoriteTextAngle = - ( startAngle + angleLen / 2 ) + 90.0;
654         while ( favoriteTextAngle <= 0.0 ) {
655             favoriteTextAngle += 360.0;
656         }
657         // flip the label when upside down
658         if ( favoriteTextAngle > 90.0 && favoriteTextAngle < 270.0 ) {
659             favoriteTextAngle = favoriteTextAngle - 180.0;
660         }
661         // negative angles can have special meaning in addLabel; otherwise they work fine
662         if ( favoriteTextAngle <= 0.0 ) {
663             favoriteTextAngle += 360.0;
664         }
665     }
666 
667     d->addLabel( lpc, index, nullptr, points, Position::Center, Position::Center,
668                  angleLen * sum / 360, favoriteTextAngle );
669 }
670 
doSpansOverlap(qreal s1Start,qreal s1End,qreal s2Start,qreal s2End)671 static bool doSpansOverlap( qreal s1Start, qreal s1End, qreal s2Start, qreal s2End )
672 {
673     if ( s1Start < s2Start ) {
674         return s1End >= s2Start;
675     } else {
676         return s1Start <= s2End;
677     }
678 }
679 
doArcsOverlap(qreal a1Start,qreal a1End,qreal a2Start,qreal a2End)680 static bool doArcsOverlap( qreal a1Start, qreal a1End, qreal a2Start, qreal a2End )
681 {
682     Q_ASSERT( a1Start >= 0 && a1Start <= 360 && a1End >= 0 && a1End <= 360 &&
683               a2Start >= 0 && a2Start <= 360 && a2End >= 0 && a2End <= 360 );
684     // all of this could probably be done better...
685     if ( a1End < a1Start ) {
686         a1End += 360;
687     }
688     if ( a2End < a2Start ) {
689         a2End += 360;
690     }
691 
692     if ( doSpansOverlap( a1Start, a1End, a2Start, a2End ) ) {
693         return true;
694     }
695     if ( a1Start > a2Start ) {
696         return doSpansOverlap( a1Start - 360.0, a1End - 360.0, a2Start, a2End );
697     } else {
698         return doSpansOverlap( a1Start + 360.0, a1End + 360.0, a2Start, a2End );
699     }
700 }
701 
draw3DEffect(QPainter * painter,const QRectF & drawPosition,uint slice)702 void PieDiagram::draw3DEffect( QPainter* painter, const QRectF& drawPosition, uint slice )
703 {
704     const QModelIndex index( model()->index( 0, slice, rootIndex() ) ); // checked
705     const ThreeDPieAttributes threeDAttrs( threeDPieAttributes( index ) );
706     if ( ! threeDAttrs.isEnabled() ) {
707         return;
708     }
709 
710     // NOTE: We cannot optimize away drawing some of the effects (even
711     // when not exploding), because some of the pies might be left out
712     // in future versions which would make some of the normally hidden
713     // pies visible. Complex hidden-line algorithms would be much more
714     // expensive than just drawing for nothing.
715 
716     // No need to save the brush, will be changed on return from this
717     // method anyway.
718     const QBrush brush = this->brush( model()->index( 0, slice, rootIndex() ) ); // checked
719     if ( threeDAttrs.useShadowColors() ) {
720         painter->setBrush( QBrush( brush.color().darker() ) );
721     } else {
722         painter->setBrush( brush );
723     }
724 
725     qreal startAngle = d->startAngles[ slice ];
726     qreal endAngle = startAngle + d->angleLens[ slice ];
727     // Normalize angles
728     while ( startAngle >= 360 )
729         startAngle -= 360;
730     while ( endAngle >= 360 )
731         endAngle -= 360;
732     Q_ASSERT( startAngle >= 0 && startAngle <= 360 );
733     Q_ASSERT( endAngle >= 0 && endAngle <= 360 );
734 
735     // positive pie height: absolute value
736     // negative pie height: relative value
737     const int depth = threeDAttrs.depth() >= 0.0 ? threeDAttrs.depth() : -threeDAttrs.depth() / 100.0 * drawPosition.height();
738 
739     if ( startAngle == endAngle || startAngle == endAngle - 360 ) { // full circle
740         draw3dOuterRim( painter, drawPosition, depth, 180, 360 );
741     } else {
742         if ( doArcsOverlap( startAngle, endAngle, 180, 360 ) ) {
743             draw3dOuterRim( painter, drawPosition, depth, startAngle, endAngle );
744         }
745 
746         if ( startAngle >= 270 || startAngle <= 90 ) {
747             draw3dCutSurface( painter, drawPosition, depth, startAngle );
748         }
749         if ( endAngle >= 90 && endAngle <= 270 ) {
750             draw3dCutSurface( painter, drawPosition, depth, endAngle );
751         }
752     }
753 }
754 
755 
draw3dCutSurface(QPainter * painter,const QRectF & rect,qreal threeDHeight,qreal angle)756 void PieDiagram::draw3dCutSurface( QPainter* painter,
757         const QRectF& rect,
758         qreal threeDHeight,
759         qreal angle )
760 {
761     QPolygonF poly( 4 );
762     const QPointF center = rect.center();
763     const QPointF circlePoint = pointOnEllipse( rect, angle );
764     poly[0] = center;
765     poly[1] = circlePoint;
766     poly[2] = QPointF( circlePoint.x(), circlePoint.y() + threeDHeight );
767     poly[3] = QPointF( center.x(), center.y() + threeDHeight );
768     // TODO: add polygon to ReverseMapper
769     painter->drawPolygon( poly );
770 }
771 
draw3dOuterRim(QPainter * painter,const QRectF & rect,qreal threeDHeight,qreal startAngle,qreal endAngle)772 void PieDiagram::draw3dOuterRim( QPainter* painter,
773         const QRectF& rect,
774         qreal threeDHeight,
775         qreal startAngle,
776         qreal endAngle )
777 {
778     // Start with getting the points for the inner arc.
779     if ( endAngle < startAngle ) {
780         endAngle += 360;
781     }
782     startAngle = qMax( startAngle, qreal( 180.0 ) );
783     endAngle = qMin( endAngle, qreal( 360.0 ) );
784 
785     int numHalfPoints = trunc( ( endAngle - startAngle ) / granularity() ) + 1;
786     if ( numHalfPoints < 2 ) {
787         return;
788     }
789 
790     QPolygonF poly( numHalfPoints );
791 
792     qreal degree = endAngle;
793     int iPoint = 0;
794     bool perfectMatch = false;
795     while ( degree >= startAngle ) {
796         poly[ numHalfPoints - iPoint - 1 ] = pointOnEllipse( rect, degree );
797 
798         perfectMatch = (degree == startAngle);
799         degree -= granularity();
800         ++iPoint;
801     }
802     // if necessary add one more point to fill the last small gap
803     if ( !perfectMatch ) {
804         poly.prepend( pointOnEllipse( rect, startAngle ) );
805         ++numHalfPoints;
806     }
807 
808     poly.resize( numHalfPoints * 2 );
809 
810     // Now copy these arcs again into the final array, but in the
811     // opposite direction and moved down by the 3D height.
812     for ( int i = numHalfPoints - 1; i >= 0; --i ) {
813         QPointF pointOnFirstArc( poly[ i ] );
814         pointOnFirstArc.setY( pointOnFirstArc.y() + threeDHeight );
815         poly[ numHalfPoints * 2 - i - 1 ] = pointOnFirstArc;
816     }
817 
818     // TODO: Add polygon to ReverseMapper
819     painter->drawPolygon( poly );
820 }
821 
findSliceAt(qreal angle,int colCount)822 uint PieDiagram::findSliceAt( qreal angle, int colCount )
823 {
824     for ( int i = 0; i < colCount; ++i ) {
825         qreal endseg = d->startAngles[ i ] + d->angleLens[ i ];
826         if ( d->startAngles[ i ] <= angle &&  endseg >= angle ) {
827             return i;
828         }
829     }
830 
831     // If we have not found it, try wrap around
832     // but only if the current searched angle is < 360 degree
833     if ( angle < 360 )
834         return findSliceAt( angle + 360, colCount );
835     // otherwise - what ever went wrong - we return 0
836     return 0;
837 }
838 
839 
findLeftSlice(uint slice,int colCount)840 uint PieDiagram::findLeftSlice( uint slice, int colCount )
841 {
842     if ( slice == 0 ) {
843         if ( colCount > 1 ) {
844             return colCount - 1;
845         } else {
846             return 0;
847         }
848     } else {
849         return slice - 1;
850     }
851 }
852 
853 
findRightSlice(uint slice,int colCount)854 uint PieDiagram::findRightSlice( uint slice, int colCount )
855 {
856     int rightSlice = slice + 1;
857     if ( rightSlice == colCount ) {
858         rightSlice = 0;
859     }
860     return rightSlice;
861 }
862 
863 
pointOnEllipse(const QRectF & boundingBox,qreal angle)864 QPointF PieDiagram::pointOnEllipse( const QRectF& boundingBox, qreal angle )
865 {
866     qreal angleRad = DEGTORAD( angle );
867     qreal cosAngle = cos( angleRad );
868     qreal sinAngle = -sin( angleRad );
869     qreal posX = cosAngle * boundingBox.width() / 2.0;
870     qreal posY = sinAngle * boundingBox.height() / 2.0;
871     return QPointF( posX + boundingBox.center().x(),
872                     posY + boundingBox.center().y() );
873 
874 }
875 
876 /*virtual*/
valueTotals() const877 qreal PieDiagram::valueTotals() const
878 {
879     if ( !model() )
880         return 0;
881     const int colCount = columnCount();
882     qreal total = 0.0;
883     // non-empty models need a row with data
884     Q_ASSERT( colCount == 0 || model()->rowCount() >= 1 );
885     for ( int j = 0; j < colCount; ++j ) {
886       total += qAbs(model()->data( model()->index( 0, j, rootIndex() ) ).toReal()); // checked
887     }
888     return total;
889 }
890 
891 /*virtual*/
numberOfValuesPerDataset() const892 qreal PieDiagram::numberOfValuesPerDataset() const
893 {
894     return model() ? model()->columnCount( rootIndex() ) : 0.0;
895 }
896 
897 /*virtual*/
numberOfGridRings() const898 qreal PieDiagram::numberOfGridRings() const
899 {
900     return 1;
901 }
902