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