KChart

KChartRingDiagram.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 "KChartRingDiagram.h"
10 #include "KChartRingDiagram_p.h"
11 
12 #include "KChartAttributesModel.h"
13 #include "KChartPaintContext.h"
14 #include "KChartPainterSaver_p.h"
15 #include "KChartPieAttributes.h"
16 #include "KChartPolarCoordinatePlane_p.h"
17 #include "KChartThreeDPieAttributes.h"
19 #include "KChartMath_p.h"
20 
21 #include <QPainter>
22 
23 using namespace KChart;
24 
25 RingDiagram::Private::Private()
26  : relativeThickness( false )
27  , expandWhenExploded( false )
28 {
29 }
30 
31 RingDiagram::Private::~Private() {}
32 
33 #define d d_func()
34 
35 RingDiagram::RingDiagram( QWidget* parent, PolarCoordinatePlane* plane ) :
36  AbstractPieDiagram( new Private(), parent, plane )
37 {
38  init();
39 }
40 
41 RingDiagram::~RingDiagram()
42 {
43 }
44 
45 void RingDiagram::init()
46 {
47 }
48 
50 {
51  return new RingDiagram( new Private( *d ) );
52 }
53 
54 bool RingDiagram::compare( const RingDiagram* other ) const
55 {
56  if ( other == this ) return true;
57  if ( ! other ) {
58  return false;
59  }
60  return // compare the base class
61  ( static_cast<const AbstractPieDiagram*>(this)->compare( other ) ) &&
62  // compare own properties
63  (relativeThickness() == other->relativeThickness()) &&
64  (expandWhenExploded() == other->expandWhenExploded());
65 }
66 
67 void RingDiagram::setRelativeThickness( bool relativeThickness )
68 {
69  d->relativeThickness = relativeThickness;
70 }
71 
72 bool RingDiagram::relativeThickness() const
73 {
74  return d->relativeThickness;
75 }
76 
77 void RingDiagram::setExpandWhenExploded( bool expand )
78 {
79  d->expandWhenExploded = expand;
80 }
81 
82 bool RingDiagram::expandWhenExploded() const
83 {
84  return d->expandWhenExploded;
85 }
86 
88 {
89  if ( !checkInvariants( true ) ) return QPair<QPointF, QPointF>( QPointF( 0, 0 ), QPointF( 0, 0 ) );
90 
91  const PieAttributes attrs( pieAttributes() );
92 
93  QPointF bottomLeft( 0, 0 );
94  QPointF topRight;
95  // If we explode, we need extra space for the pie slice that has the largest explosion distance.
96  if ( attrs.explode() ) {
97  const int rCount = rowCount();
98  const int colCount = columnCount();
99  qreal maxExplode = 0.0;
100  for ( int i = 0; i < rCount; ++i ) {
101  qreal maxExplodeInThisRow = 0.0;
102  for ( int j = 0; j < colCount; ++j ) {
103  const PieAttributes columnAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); // checked
104  maxExplodeInThisRow = qMax( maxExplodeInThisRow, columnAttrs.explodeFactor() );
105  }
106  maxExplode += maxExplodeInThisRow;
107 
108  // FIXME: What if explode factor of inner ring is > 1.0 ?
109  if ( !d->expandWhenExploded ) {
110  break;
111  }
112  }
113  // explode factor is relative to width (outer r - inner r) of one ring
114  maxExplode /= ( rCount + 1);
115  topRight = QPointF( 1.0 + maxExplode, 1.0 + maxExplode );
116  } else {
117  topRight = QPointF( 1.0, 1.0 );
118  }
119  return QPair<QPointF, QPointF>( bottomLeft, topRight );
120 }
121 
122 void RingDiagram::paintEvent( QPaintEvent* )
123 {
124  QPainter painter ( viewport() );
125  PaintContext ctx;
126  ctx.setPainter ( &painter );
127  ctx.setRectangle( QRectF ( 0, 0, width(), height() ) );
128  paint ( &ctx );
129 }
130 
131 void RingDiagram::resizeEvent( QResizeEvent* )
132 {
133 }
134 
136 {
137  // note: Not having any data model assigned is no bug
138  // but we can not draw a diagram then either.
139  if ( !checkInvariants(true) )
140  return;
141 
142  d->reverseMapper.clear();
143 
144  const PieAttributes attrs( pieAttributes() );
145 
146  const int rCount = rowCount();
147  const int colCount = columnCount();
148 
149  //QRectF contentsRect = PolarCoordinatePlane::Private::contentsRect( polarCoordinatePlane() );
150  QRectF contentsRect = ctx->rectangle();
151  if ( contentsRect.isEmpty() )
152  return;
153 
154  d->startAngles = QVector< QVector<qreal> >( rCount, QVector<qreal>( colCount ) );
155  d->angleLens = QVector< QVector<qreal> >( rCount, QVector<qreal>( colCount ) );
156 
157  // compute position
158  d->size = qMin( contentsRect.width(), contentsRect.height() ); // initial size
159 
160  // if the slices explode, we need to give them additional space =>
161  // make the basic size smaller
162  qreal totalOffset = 0.0;
163  for ( int i = 0; i < rCount; ++i ) {
164  qreal maxOffsetInThisRow = 0.0;
165  for ( int j = 0; j < colCount; ++j ) {
166  const PieAttributes cellAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); // checked
167  //qDebug() << cellAttrs.explodeFactor();
168  const qreal explode = cellAttrs.explode() ? cellAttrs.explodeFactor() : 0.0;
169  maxOffsetInThisRow = qMax( maxOffsetInThisRow, cellAttrs.gapFactor( false ) + explode );
170  }
171  if ( !d->expandWhenExploded ) {
172  maxOffsetInThisRow -= qreal( i );
173  }
174  totalOffset += qMax<qreal>( maxOffsetInThisRow, 0.0 );
175  // FIXME: What if explode factor of inner ring is > 1.0 ?
176  //if ( !d->expandWhenExploded )
177  // break;
178  }
179 
180  // explode factor is relative to width (outer r - inner r) of one ring
181  if ( rCount > 0 )
182  totalOffset /= ( rCount + 1 );
183  d->size /= ( 1.0 + totalOffset );
184 
185 
186  qreal x = ( contentsRect.width() == d->size ) ? 0.0 : ( ( contentsRect.width() - d->size ) / 2.0 );
187  qreal y = ( contentsRect.height() == d->size ) ? 0.0 : ( ( contentsRect.height() - d->size ) / 2.0 );
188  d->position = QRectF( x, y, d->size, d->size );
189  d->position.translate( contentsRect.left(), contentsRect.top() );
190 
191  const PolarCoordinatePlane * plane = polarCoordinatePlane();
192 
193  d->forgetAlreadyPaintedDataValues();
194  for ( int iRow = 0; iRow < rCount; ++iRow ) {
195  const qreal sum = valueTotals( iRow );
196  if ( sum == 0.0 ) //nothing to draw
197  continue;
198  qreal currentValue = plane ? plane->startPosition() : 0.0;
199  const qreal sectorsPerValue = 360.0 / sum;
200 
201  for ( int iColumn = 0; iColumn < colCount; ++iColumn ) {
202  // is there anything at all at this column?
203  bool bOK;
204  const qreal cellValue = qAbs( model()->data( model()->index( iRow, iColumn, rootIndex() ) ) // checked
205  .toReal( &bOK ) );
206 
207  if ( bOK ) {
208  d->startAngles[ iRow ][ iColumn ] = currentValue;
209  d->angleLens[ iRow ][ iColumn ] = cellValue * sectorsPerValue;
210  } else { // mark as non-existent
211  d->angleLens[ iRow ][ iColumn ] = 0.0;
212  if ( iColumn > 0.0 ) {
213  d->startAngles[ iRow ][ iColumn ] = d->startAngles[ iRow ][ iColumn - 1 ];
214  } else {
215  d->startAngles[ iRow ][ iColumn ] = currentValue;
216  }
217  }
218 
219  currentValue = d->startAngles[ iRow ][ iColumn ] + d->angleLens[ iRow ][ iColumn ];
220 
221  drawOneSlice( ctx->painter(), iRow, iColumn, granularity() );
222  }
223  }
224 }
225 
226 #if defined ( Q_WS_WIN)
227 #define trunc(x) ((int)(x))
228 #endif
229 
230 void RingDiagram::drawOneSlice( QPainter* painter, uint dataset, uint slice, qreal granularity )
231 {
232  // Is there anything to draw at all?
233  const qreal angleLen = d->angleLens[ dataset ][ slice ];
234  if ( angleLen ) {
235  drawPieSurface( painter, dataset, slice, granularity );
236  }
237 }
238 
239 void RingDiagram::resize( const QSizeF& size)
240 {
242 }
243 
244 void RingDiagram::drawPieSurface( QPainter* painter, uint dataset, uint slice, qreal granularity )
245 {
246  // Is there anything to draw at all?
247  qreal angleLen = d->angleLens[ dataset ][ slice ];
248  if ( angleLen ) {
249  qreal startAngle = d->startAngles[ dataset ][ slice ];
250 
251  QModelIndex index( model()->index( dataset, slice, rootIndex() ) ); // checked
252  const PieAttributes attrs( pieAttributes( index ) );
253  const ThreeDPieAttributes threeDAttrs( threeDPieAttributes( index ) );
254 
255  const int rCount = rowCount();
256  const int colCount = columnCount();
257 
258  int iPoint = 0;
259 
260  QRectF drawPosition = d->position;
261 
263 
264  QBrush br = brush( index );
265  if ( threeDAttrs.isEnabled() ) {
266  br = threeDAttrs.threeDBrush( br, drawPosition );
267  }
268  painter->setBrush( br );
269 
270  painter->setPen( pen( index ) );
271 
272  if ( angleLen == 360 ) {
273  // full circle, avoid nasty line in the middle
274  // FIXME: Draw a complete ring here
275  //painter->drawEllipse( drawPosition );
276  } else {
277  bool perfectMatch = false;
278 
279  qreal circularGap = 0.0;
280 
281  if ( attrs.gapFactor( true ) > 0.0 ) {
282  // FIXME: Measure in degrees!
283  circularGap = attrs.gapFactor( true );
284  }
285 
286  QPolygonF poly;
287 
288  qreal degree = 0;
289 
290  qreal actualStartAngle = startAngle + circularGap;
291  qreal actualAngleLen = angleLen - 2 * circularGap;
292 
293  qreal totalRadialExplode = 0.0;
294  qreal maxRadialExplode = 0.0;
295 
296  qreal totalRadialGap = 0.0;
297  qreal maxRadialGap = 0.0;
298  for ( uint i = rCount - 1; i > dataset; --i ) {
299  qreal maxRadialExplodeInThisRow = 0.0;
300  qreal maxRadialGapInThisRow = 0.0;
301  for ( int j = 0; j < colCount; ++j ) {
302  const PieAttributes cellAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); // checked
303  if ( d->expandWhenExploded ) {
304  maxRadialGapInThisRow = qMax( maxRadialGapInThisRow, cellAttrs.gapFactor( false ) );
305  }
306 
307  // Don't use a gap for the very inner circle
308  if ( cellAttrs.explode() && d->expandWhenExploded ) {
309  maxRadialExplodeInThisRow = qMax( maxRadialExplodeInThisRow, cellAttrs.explodeFactor() );
310  }
311  }
312  maxRadialExplode += maxRadialExplodeInThisRow;
313  maxRadialGap += maxRadialGapInThisRow;
314 
315  // FIXME: What if explode factor of inner ring is > 1.0 ?
316  //if ( !d->expandWhenExploded )
317  // break;
318  }
319  totalRadialGap = maxRadialGap + attrs.gapFactor( false );
320  totalRadialExplode = attrs.explode() ? maxRadialExplode + attrs.explodeFactor() : maxRadialExplode;
321 
322  while ( degree <= actualAngleLen ) {
323  const QPointF p = pointOnEllipse( drawPosition, dataset, slice, false, actualStartAngle + degree,
324  totalRadialGap, totalRadialExplode );
325  poly.append( p );
326  degree += granularity;
327  iPoint++;
328  }
329  if ( ! perfectMatch ) {
330  poly.append( pointOnEllipse( drawPosition, dataset, slice, false, actualStartAngle + actualAngleLen,
331  totalRadialGap, totalRadialExplode ) );
332  iPoint++;
333  }
334 
335  // The center point of the inner brink
336  const QPointF innerCenterPoint( poly[ int(iPoint / 2) ] );
337 
338  actualStartAngle = startAngle + circularGap;
339  actualAngleLen = angleLen - 2 * circularGap;
340 
341  degree = actualAngleLen;
342 
343  const int lastInnerBrinkPoint = iPoint;
344  while ( degree >= 0 ) {
345  poly.append( pointOnEllipse( drawPosition, dataset, slice, true, actualStartAngle + degree,
346  totalRadialGap, totalRadialExplode ) );
347  perfectMatch = (degree == 0);
348  degree -= granularity;
349  iPoint++;
350  }
351  // if necessary add one more point to fill the last small gap
352  if ( ! perfectMatch ) {
353  poly.append( pointOnEllipse( drawPosition, dataset, slice, true, actualStartAngle,
354  totalRadialGap, totalRadialExplode ) );
355  iPoint++;
356  }
357 
358  // The center point of the outer brink
359  const QPointF outerCenterPoint( poly[ lastInnerBrinkPoint + int((iPoint - lastInnerBrinkPoint) / 2) ] );
360  //qDebug() << poly;
361  //find the value and paint it
362  //fix value position
363  const qreal sum = valueTotals( dataset );
364  painter->drawPolygon( poly );
365 
366  d->reverseMapper.addPolygon( index.row(), index.column(), poly );
367 
368  const QPointF centerPoint = (innerCenterPoint + outerCenterPoint) / 2.0;
369 
370  const PainterSaver ps( painter );
371  const TextAttributes ta = dataValueAttributes( index ).textAttributes();
372  if ( !ta.hasRotation() && autoRotateLabels() )
373  {
374  const QPointF& p1 = poly.last();
375  const QPointF& p2 = poly[ lastInnerBrinkPoint ];
376  const QLineF line( p1, p2 );
377  // TODO: do the label rotation like in PieDiagram
378  const qreal angle = line.dx() == 0 ? 0.0 : atan( line.dy() / line.dx() );
379  painter->translate( centerPoint );
380  painter->rotate( angle / 2.0 / 3.141592653589793 * 360.0 );
381  painter->translate( -centerPoint );
382  }
383 
384  paintDataValueText( painter, index, centerPoint, angleLen*sum / 360 );
385  }
386  }
387 }
388 
389 
390 QPointF RingDiagram::pointOnEllipse( const QRectF& rect, int dataset, int slice, bool outer, qreal angle,
391  qreal totalGapFactor, qreal totalExplodeFactor )
392 {
393  qreal angleLen = d->angleLens[ dataset ][ slice ];
394  qreal startAngle = d->startAngles[ dataset ][ slice ];
395 
396  const int rCount = rowCount() * 2;
397 
398  qreal level = outer ? ( rCount - dataset - 1 ) + 2 : ( rCount - dataset - 1 ) + 1;
399 
400  const qreal offsetX = rCount > 0 ? level * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0;
401  const qreal offsetY = rCount > 0 ? level * rect.height() / ( ( rCount + 1 ) * 2 ) : 0.0;
402  const qreal centerOffsetX = rCount > 0 ? totalExplodeFactor * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0;
403  const qreal centerOffsetY = rCount > 0 ? totalExplodeFactor * rect.height() / ( ( rCount + 1 ) * 2 ) : 0.0;
404  const qreal gapOffsetX = rCount > 0 ? totalGapFactor * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0;
405  const qreal gapOffsetY = rCount > 0 ? totalGapFactor * rect.height() / ( ( rCount + 1 ) * 2 ) : 0.0;
406 
407  qreal explodeAngleRad = DEGTORAD( angle );
408  qreal cosAngle = cos( explodeAngleRad );
409  qreal sinAngle = -sin( explodeAngleRad );
410  qreal explodeAngleCenterRad = DEGTORAD( startAngle + angleLen / 2.0 );
411  qreal cosAngleCenter = cos( explodeAngleCenterRad );
412  qreal sinAngleCenter = -sin( explodeAngleCenterRad );
413  return QPointF( ( offsetX + gapOffsetX ) * cosAngle + centerOffsetX * cosAngleCenter + rect.center().x(),
414  ( offsetY + gapOffsetY ) * sinAngle + centerOffsetY * sinAngleCenter + rect.center().y() );
415 }
416 
417 /*virtual*/
419 {
420  const int rCount = rowCount();
421  const int colCount = columnCount();
422  qreal total = 0.0;
423  for ( int i = 0; i < rCount; ++i ) {
424  for ( int j = 0; j < colCount; ++j ) {
425  total += qAbs( model()->data( model()->index( i, j, rootIndex() ) ).toReal() ); // checked
426  }
427  }
428  return total;
429 }
430 
431 qreal RingDiagram::valueTotals( int dataset ) const
432 {
433  Q_ASSERT( dataset < model()->rowCount() );
434  const int colCount = columnCount();
435  qreal total = 0.0;
436  for ( int j = 0; j < colCount; ++j ) {
437  total += qAbs( model()->data( model()->index( dataset, j, rootIndex() ) ).toReal() ); // checked
438  }
439  return total;
440 }
441 
442 /*virtual*/
444 {
445  return model() ? model()->columnCount( rootIndex() ) : 0.0;
446 }
447 
448 qreal RingDiagram::numberOfDatasets() const
449 {
450  return model() ? model()->rowCount( rootIndex() ) : 0.0;
451 }
452 
453 /*virtual*/
455 {
456  return 1;
457 }
A set of 3D pie attributes.
void setPen(const QColor &color)
virtual int rowCount(const QModelIndex &parent) const const=0
T & last()
qreal numberOfGridRings() const override
\reimpl
void rotate(qreal angle)
qreal startPosition() const
Retrieve the rotation of the coordinate plane.
A set of attributes controlling the appearance of pie charts.
QAbstractItemModel * model() const const
void append(const T &value)
Declaring the class KChart::DataValueAttributes.
Class only listed here to document inheritance of some KChart classes.
QStringView level(QStringView ifopt)
const QPair< QPointF, QPointF > calculateDataBoundaries() const override
\reimpl
int width() const const
Base class for any diagram type.
Stores information about painting diagrams.
bool compare(const RingDiagram *other) const
Returns true if both diagrams have the same settings.
qreal numberOfValuesPerDataset() const override
\reimpl
int left() const const
void drawPolygon(const QPointF *points, int pointCount, Qt::FillRule fillRule)
qreal valueTotals() const override
\reimpl
void resize(const QSizeF &area) override
\reimpl
int top() const const
RingDiagram defines a common ring diagram.
void init(KXmlGuiWindow *window, KgDifficulty *difficulty=nullptr)
virtual int columnCount(const QModelIndex &parent) const const=0
DataValueAttributes dataValueAttributes() const
Retrieve the DataValueAttributes specified globally.
QModelIndex rootIndex() const const
void setBrush(const QBrush &brush)
int height() const const
bool isEmpty() const const
void translate(const QPointF &offset)
virtual void resize(const QSizeF &area)
Called by the widget's sizeEvent.
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.
void setRenderHint(QPainter::RenderHint hint, bool on)
QWidget * viewport() const const
A set of text attributes.
void paint(PaintContext *paintContext) override
\reimpl
QRect contentsRect() const const
virtual RingDiagram * clone() const
Creates an exact copy of this diagram.
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Fri Jul 1 2022 05:09:20 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.