KChart

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

KDE's Doxygen guidelines are available online.