KD Chart 2  [rev.2.7]
KDChartRingDiagram.cpp
Go to the documentation of this file.
1 /****************************************************************************
2 ** Copyright (C) 2001-2020 Klaralvdalens Datakonsult AB. All rights reserved.
3 **
4 ** This file is part of the KD Chart library.
5 **
6 ** Licensees holding valid commercial KD Chart licenses may use this file in
7 ** accordance with the KD Chart Commercial License Agreement provided with
8 ** the Software.
9 **
10 **
11 ** This file may be distributed and/or modified under the terms of the
12 ** GNU General Public License version 2 and version 3 as published by the
13 ** Free Software Foundation and appearing in the file LICENSE.GPL.txt included.
14 **
15 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
16 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
17 **
18 ** Contact info@kdab.com if any conditions of this licensing are not
19 ** clear to you.
20 **
21 **********************************************************************/
22 
23 #include "KDChartRingDiagram.h"
24 #include "KDChartRingDiagram_p.h"
25 
26 #include "KDChartAttributesModel.h"
27 #include "KDChartPaintContext.h"
28 #include "KDChartPainterSaver_p.h"
29 #include "KDChartPieAttributes.h"
30 #include "KDChartPolarCoordinatePlane_p.h"
33 
34 #include <QPainter>
35 
36 #include <KDABLibFakes>
37 
38 using namespace KDChart;
39 
40 RingDiagram::Private::Private()
41  : relativeThickness( false )
42  , expandWhenExploded( false )
43 {
44 }
45 
46 RingDiagram::Private::~Private() {}
47 
48 #define d d_func()
49 
51  AbstractPieDiagram( new Private(), parent, plane )
52 {
53  init();
54 }
55 
57 {
58 }
59 
60 void RingDiagram::init()
61 {
62 }
63 
68 {
69  return new RingDiagram( new Private( *d ) );
70 }
71 
72 bool RingDiagram::compare( const RingDiagram* other ) const
73 {
74  if ( other == this ) return true;
75  if ( ! other ) {
76  return false;
77  }
78  return // compare the base class
79  ( static_cast<const AbstractPieDiagram*>(this)->compare( other ) ) &&
80  // compare own properties
81  (relativeThickness() == other->relativeThickness()) &&
82  (expandWhenExploded() == other->expandWhenExploded());
83 }
84 
86 {
87  d->relativeThickness = relativeThickness;
88 }
89 
91 {
92  return d->relativeThickness;
93 }
94 
96 {
97  d->expandWhenExploded = expand;
98 }
99 
101 {
102  return d->expandWhenExploded;
103 }
104 
106 {
107  if ( !checkInvariants( true ) ) return QPair<QPointF, QPointF>( QPointF( 0, 0 ), QPointF( 0, 0 ) );
108 
109  const PieAttributes attrs( pieAttributes() );
110 
111  QPointF bottomLeft( 0, 0 );
112  QPointF topRight;
113  // If we explode, we need extra space for the pie slice that has the largest explosion distance.
114  if ( attrs.explode() ) {
115  const int rCount = rowCount();
116  const int colCount = columnCount();
117  qreal maxExplode = 0.0;
118  for ( int i = 0; i < rCount; ++i ) {
119  qreal maxExplodeInThisRow = 0.0;
120  for ( int j = 0; j < colCount; ++j ) {
121  const PieAttributes columnAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); // checked
122  maxExplodeInThisRow = qMax( maxExplodeInThisRow, columnAttrs.explodeFactor() );
123  }
124  maxExplode += maxExplodeInThisRow;
125 
126  // FIXME: What if explode factor of inner ring is > 1.0 ?
127  if ( !d->expandWhenExploded ) {
128  break;
129  }
130  }
131  // explode factor is relative to width (outer r - inner r) of one ring
132  maxExplode /= ( rCount + 1);
133  topRight = QPointF( 1.0 + maxExplode, 1.0 + maxExplode );
134  } else {
135  topRight = QPointF( 1.0, 1.0 );
136  }
137  return QPair<QPointF, QPointF>( bottomLeft, topRight );
138 }
139 
140 void RingDiagram::paintEvent( QPaintEvent* )
141 {
142  QPainter painter ( viewport() );
143  PaintContext ctx;
144  ctx.setPainter ( &painter );
145  ctx.setRectangle( QRectF ( 0, 0, width(), height() ) );
146  paint ( &ctx );
147 }
148 
149 void RingDiagram::resizeEvent( QResizeEvent* )
150 {
151 }
152 
154 {
155  // note: Not having any data model assigned is no bug
156  // but we can not draw a diagram then either.
157  if ( !checkInvariants(true) )
158  return;
159 
160  d->reverseMapper.clear();
161 
162  const PieAttributes attrs( pieAttributes() );
163 
164  const int rCount = rowCount();
165  const int colCount = columnCount();
166 
167  //QRectF contentsRect = PolarCoordinatePlane::Private::contentsRect( polarCoordinatePlane() );
168  QRectF contentsRect = ctx->rectangle();
169  if ( contentsRect.isEmpty() )
170  return;
171 
172  d->startAngles = QVector< QVector<qreal> >( rCount, QVector<qreal>( colCount ) );
173  d->angleLens = QVector< QVector<qreal> >( rCount, QVector<qreal>( colCount ) );
174 
175  // compute position
176  d->size = qMin( contentsRect.width(), contentsRect.height() ); // initial size
177 
178  // if the slices explode, we need to give them additional space =>
179  // make the basic size smaller
180  qreal totalOffset = 0.0;
181  for ( int i = 0; i < rCount; ++i ) {
182  qreal maxOffsetInThisRow = 0.0;
183  for ( int j = 0; j < colCount; ++j ) {
184  const PieAttributes cellAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); // checked
185  //qDebug() << cellAttrs.explodeFactor();
186  const qreal explode = cellAttrs.explode() ? cellAttrs.explodeFactor() : 0.0;
187  maxOffsetInThisRow = qMax( maxOffsetInThisRow, cellAttrs.gapFactor( false ) + explode );
188  }
189  if ( !d->expandWhenExploded ) {
190  maxOffsetInThisRow -= qreal( i );
191  }
192  totalOffset += qMax( maxOffsetInThisRow, (qreal)0.0 );
193  // FIXME: What if explode factor of inner ring is > 1.0 ?
194  //if ( !d->expandWhenExploded )
195  // break;
196  }
197 
198  // explode factor is relative to width (outer r - inner r) of one ring
199  if ( rCount > 0 )
200  totalOffset /= ( rCount + 1 );
201  d->size /= ( 1.0 + totalOffset );
202 
203 
204  qreal x = ( contentsRect.width() == d->size ) ? 0.0 : ( ( contentsRect.width() - d->size ) / 2.0 );
205  qreal y = ( contentsRect.height() == d->size ) ? 0.0 : ( ( contentsRect.height() - d->size ) / 2.0 );
206  d->position = QRectF( x, y, d->size, d->size );
207  d->position.translate( contentsRect.left(), contentsRect.top() );
208 
209  const PolarCoordinatePlane * plane = polarCoordinatePlane();
210 
211  QVariant vValY;
212 
213  d->forgetAlreadyPaintedDataValues();
214  for ( int iRow = 0; iRow < rCount; ++iRow ) {
215  const qreal sum = valueTotals( iRow );
216  if ( sum == 0.0 ) //nothing to draw
217  continue;
218  qreal currentValue = plane ? plane->startPosition() : 0.0;
219  const qreal sectorsPerValue = 360.0 / sum;
220 
221  for ( int iColumn = 0; iColumn < colCount; ++iColumn ) {
222  // is there anything at all at this column?
223  bool bOK;
224  const qreal cellValue = qAbs( model()->data( model()->index( iRow, iColumn, rootIndex() ) ) // checked
225  .toReal( &bOK ) );
226 
227  if ( bOK ) {
228  d->startAngles[ iRow ][ iColumn ] = currentValue;
229  d->angleLens[ iRow ][ iColumn ] = cellValue * sectorsPerValue;
230  } else { // mark as non-existent
231  d->angleLens[ iRow ][ iColumn ] = 0.0;
232  if ( iColumn > 0.0 ) {
233  d->startAngles[ iRow ][ iColumn ] = d->startAngles[ iRow ][ iColumn - 1 ];
234  } else {
235  d->startAngles[ iRow ][ iColumn ] = currentValue;
236  }
237  }
238 
239  currentValue = d->startAngles[ iRow ][ iColumn ] + d->angleLens[ iRow ][ iColumn ];
240 
241  drawOneSlice( ctx->painter(), iRow, iColumn, granularity() );
242  }
243  }
244 }
245 
246 #if defined ( Q_WS_WIN)
247 #define trunc(x) ((int)(x))
248 #endif
249 
255 void RingDiagram::drawOneSlice( QPainter* painter, uint dataset, uint slice, qreal granularity )
256 {
257  // Is there anything to draw at all?
258  const qreal angleLen = d->angleLens[ dataset ][ slice ];
259  if ( angleLen ) {
260  drawPieSurface( painter, dataset, slice, granularity );
261  }
262 }
263 
264 void RingDiagram::resize( const QSizeF& )
265 {
266 }
267 
275 void RingDiagram::drawPieSurface( QPainter* painter, uint dataset, uint slice, qreal granularity )
276 {
277  // Is there anything to draw at all?
278  qreal angleLen = d->angleLens[ dataset ][ slice ];
279  if ( angleLen ) {
280  qreal startAngle = d->startAngles[ dataset ][ slice ];
281 
282  QModelIndex index( model()->index( dataset, slice, rootIndex() ) ); // checked
283  const PieAttributes attrs( pieAttributes( index ) );
284  const ThreeDPieAttributes threeDAttrs( threeDPieAttributes( index ) );
285 
286  const int rCount = rowCount();
287  const int colCount = columnCount();
288 
289  int iPoint = 0;
290 
291  QRectF drawPosition = d->position;
292 
293  painter->setRenderHint ( QPainter::Antialiasing );
294 
295  QBrush br = brush( index );
296  if ( threeDAttrs.isEnabled() ) {
297  br = threeDAttrs.threeDBrush( br, drawPosition );
298  }
299  painter->setBrush( br );
300 
301  painter->setPen( pen( index ) );
302 
303  if ( angleLen == 360 ) {
304  // full circle, avoid nasty line in the middle
305  // FIXME: Draw a complete ring here
306  //painter->drawEllipse( drawPosition );
307  } else {
308  bool perfectMatch = false;
309 
310  qreal circularGap = 0.0;
311 
312  if ( attrs.gapFactor( true ) > 0.0 ) {
313  // FIXME: Measure in degrees!
314  circularGap = attrs.gapFactor( true );
315  }
316 
317  QPolygonF poly;
318 
319  qreal degree = 0;
320 
321  qreal actualStartAngle = startAngle + circularGap;
322  qreal actualAngleLen = angleLen - 2 * circularGap;
323 
324  qreal totalRadialExplode = 0.0;
325  qreal maxRadialExplode = 0.0;
326 
327  qreal totalRadialGap = 0.0;
328  qreal maxRadialGap = 0.0;
329  for ( uint i = rCount - 1; i > dataset; --i ) {
330  qreal maxRadialExplodeInThisRow = 0.0;
331  qreal maxRadialGapInThisRow = 0.0;
332  for ( int j = 0; j < colCount; ++j ) {
333  const PieAttributes cellAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); // checked
334  if ( d->expandWhenExploded ) {
335  maxRadialGapInThisRow = qMax( maxRadialGapInThisRow, cellAttrs.gapFactor( false ) );
336  }
337 
338  // Don't use a gap for the very inner circle
339  if ( cellAttrs.explode() && d->expandWhenExploded ) {
340  maxRadialExplodeInThisRow = qMax( maxRadialExplodeInThisRow, cellAttrs.explodeFactor() );
341  }
342  }
343  maxRadialExplode += maxRadialExplodeInThisRow;
344  maxRadialGap += maxRadialGapInThisRow;
345 
346  // FIXME: What if explode factor of inner ring is > 1.0 ?
347  //if ( !d->expandWhenExploded )
348  // break;
349  }
350  totalRadialGap = maxRadialGap + attrs.gapFactor( false );
351  totalRadialExplode = attrs.explode() ? maxRadialExplode + attrs.explodeFactor() : maxRadialExplode;
352 
353  while ( degree <= actualAngleLen ) {
354  const QPointF p = pointOnEllipse( drawPosition, dataset, slice, false, actualStartAngle + degree,
355  totalRadialGap, totalRadialExplode );
356  poly.append( p );
357  degree += granularity;
358  iPoint++;
359  }
360  if ( ! perfectMatch ) {
361  poly.append( pointOnEllipse( drawPosition, dataset, slice, false, actualStartAngle + actualAngleLen,
362  totalRadialGap, totalRadialExplode ) );
363  iPoint++;
364  }
365 
366  // The center point of the inner brink
367  const QPointF innerCenterPoint( poly[ int(iPoint / 2) ] );
368 
369  actualStartAngle = startAngle + circularGap;
370  actualAngleLen = angleLen - 2 * circularGap;
371 
372  degree = actualAngleLen;
373 
374  const int lastInnerBrinkPoint = iPoint;
375  while ( degree >= 0 ) {
376  poly.append( pointOnEllipse( drawPosition, dataset, slice, true, actualStartAngle + degree,
377  totalRadialGap, totalRadialExplode ) );
378  perfectMatch = (degree == 0);
379  degree -= granularity;
380  iPoint++;
381  }
382  // if necessary add one more point to fill the last small gap
383  if ( ! perfectMatch ) {
384  poly.append( pointOnEllipse( drawPosition, dataset, slice, true, actualStartAngle,
385  totalRadialGap, totalRadialExplode ) );
386  iPoint++;
387  }
388 
389  // The center point of the outer brink
390  const QPointF outerCenterPoint( poly[ lastInnerBrinkPoint + int((iPoint - lastInnerBrinkPoint) / 2) ] );
391  //qDebug() << poly;
392  //find the value and paint it
393  //fix value position
394  const qreal sum = valueTotals( dataset );
395  painter->drawPolygon( poly );
396 
397  d->reverseMapper.addPolygon( index.row(), index.column(), poly );
398 
399  const QPointF centerPoint = (innerCenterPoint + outerCenterPoint) / 2.0;
400 
401  const PainterSaver ps( painter );
402  const TextAttributes ta = dataValueAttributes( index ).textAttributes();
403  if ( !ta.hasRotation() && autoRotateLabels() )
404  {
405  const QPointF& p1 = poly.last();
406  const QPointF& p2 = poly[ lastInnerBrinkPoint ];
407  const QLineF line( p1, p2 );
408  // TODO: do the label rotation like in PieDiagram
409  const qreal angle = line.dx() == 0 ? 0.0 : atan( line.dy() / line.dx() );
410  painter->translate( centerPoint );
411  painter->rotate( angle / 2.0 / 3.141592653589793 * 360.0 );
412  painter->translate( -centerPoint );
413  }
414 
415  paintDataValueText( painter, index, centerPoint, angleLen*sum / 360 );
416  }
417  }
418 }
419 
420 
425 QPointF RingDiagram::pointOnEllipse( const QRectF& rect, int dataset, int slice, bool outer, qreal angle,
426  qreal totalGapFactor, qreal totalExplodeFactor )
427 {
428  qreal angleLen = d->angleLens[ dataset ][ slice ];
429  qreal startAngle = d->startAngles[ dataset ][ slice ];
430 
431  const int rCount = rowCount() * 2;
432 
433  qreal level = outer ? ( rCount - dataset - 1 ) + 2 : ( rCount - dataset - 1 ) + 1;
434 
435  const qreal offsetX = rCount > 0 ? level * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0;
436  const qreal offsetY = rCount > 0 ? level * rect.height() / ( ( rCount + 1 ) * 2 ) : 0.0;
437  const qreal centerOffsetX = rCount > 0 ? totalExplodeFactor * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0;
438  const qreal centerOffsetY = rCount > 0 ? totalExplodeFactor * rect.height() / ( ( rCount + 1 ) * 2 ) : 0.0;
439  const qreal gapOffsetX = rCount > 0 ? totalGapFactor * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0;
440  const qreal gapOffsetY = rCount > 0 ? totalGapFactor * rect.height() / ( ( rCount + 1 ) * 2 ) : 0.0;
441 
442  qreal explodeAngleRad = DEGTORAD( angle );
443  qreal cosAngle = cos( explodeAngleRad );
444  qreal sinAngle = -sin( explodeAngleRad );
445  qreal explodeAngleCenterRad = DEGTORAD( startAngle + angleLen / 2.0 );
446  qreal cosAngleCenter = cos( explodeAngleCenterRad );
447  qreal sinAngleCenter = -sin( explodeAngleCenterRad );
448  return QPointF( ( offsetX + gapOffsetX ) * cosAngle + centerOffsetX * cosAngleCenter + rect.center().x(),
449  ( offsetY + gapOffsetY ) * sinAngle + centerOffsetY * sinAngleCenter + rect.center().y() );
450 }
451 
452 /*virtual*/
454 {
455  const int rCount = rowCount();
456  const int colCount = columnCount();
457  qreal total = 0.0;
458  for ( int i = 0; i < rCount; ++i ) {
459  for ( int j = 0; j < colCount; ++j ) {
460  total += qAbs( model()->data( model()->index( i, j, rootIndex() ) ).toReal() ); // checked
461  }
462  }
463  return total;
464 }
465 
466 qreal RingDiagram::valueTotals( int dataset ) const
467 {
468  Q_ASSERT( dataset < model()->rowCount() );
469  const int colCount = columnCount();
470  qreal total = 0.0;
471  for ( int j = 0; j < colCount; ++j ) {
472  total += qAbs( model()->data( model()->index( dataset, j, rootIndex() ) ).toReal() ); // checked
473  }
474  return total;
475 }
476 
477 /*virtual*/
479 {
480  return model() ? model()->columnCount( rootIndex() ) : 0.0;
481 }
482 
484 {
485  return model() ? model()->rowCount( rootIndex() ) : 0.0;
486 }
487 
488 /*virtual*/
490 {
491  return 1;
492 }
DataValueAttributes dataValueAttributes() const
Retrieve the DataValueAttributes specified globally.
void resize(const QSizeF &area) override
[reimplemented]
virtual bool expandWhenExploded() const
void paint(PaintContext *paintContext) override
[reimplemented]
const QRectF rectangle() const
void setPainter(QPainter *painter)
virtual bool checkInvariants(bool justReturnTheStatus=false) const
qreal valueTotals() const override
[reimplemented]
Declaring the class KDChart::DataValueAttributes.
void resizeEvent(QResizeEvent *) override
QPainter * painter() const
virtual void setExpandWhenExploded(bool expand)
RingDiagram(QWidget *parent=0, PolarCoordinatePlane *plane=0)
#define d
virtual RingDiagram * clone() const
Creates an exact copy of this diagram.
QBrush brush() const
Retrieve the brush to be used for painting datapoints globally.
A set of attributes controlling the appearance of pie charts.
qreal numberOfDatasets() const override
const QPair< QPointF, QPointF > calculateDataBoundaries() const override
[reimplemented]
const PolarCoordinatePlane * polarCoordinatePlane() const
bool compare(const RingDiagram *other) const
Returns true if both diagrams have the same settings.
void setRelativeThickness(bool relativeThickness)
Stores information about painting diagrams.
A set of 3D pie attributes.
qreal numberOfGridRings() const override
[reimplemented]
Base class for any diagram type.
ThreeDPieAttributes threeDPieAttributes() const
void paintDataValueText(QPainter *painter, const QModelIndex &index, const QPointF &pos, qreal value)
QPen pen() const
Retrieve the pen to be used for painting datapoints globally.
virtual QBrush threeDBrush(const QBrush &brush, const QRectF &rect) const
qreal startPosition() const
Retrieve the rotation of the coordinate plane.
void paintEvent(QPaintEvent *) override
void setRectangle(const QRectF &rect)
Class only listed here to document inheritance of some KDChart classes.
RingDiagram defines a common ring diagram.
qreal numberOfValuesPerDataset() const override
[reimplemented]
A set of text attributes.
bool relativeThickness() const
qreal gapFactor(bool circular) const

Klarälvdalens Datakonsult AB (KDAB)
"The Qt, C++ and OpenGL Experts"
https://www.kdab.com/

https://www.kdab.com/development-resources/qt-tools/kd-chart/