/**************************************************************************** ** Copyright (C) 2001-2011 Klaralvdalens Datakonsult AB. All rights reserved. ** ** This file is part of the KD Chart library. ** ** Licensees holding valid commercial KD Chart licenses may use this file in ** accordance with the KD Chart Commercial License Agreement provided with ** the Software. ** ** ** This file may be distributed and/or modified under the terms of the ** GNU General Public License version 2 and version 3 as published by the ** Free Software Foundation and appearing in the file LICENSE.GPL.txt included. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ** Contact info@kdab.com if any conditions of this licensing are not ** clear to you. ** **********************************************************************/ #include "KDChartRingDiagram.h" #include "KDChartRingDiagram_p.h" #include "KDChartAttributesModel.h" #include "KDChartPaintContext.h" #include "KDChartPainterSaver_p.h" #include "KDChartPieAttributes.h" #include "KDChartDataValueAttributes.h" #include #include using namespace KDChart; RingDiagram::Private::Private() : relativeThickness( false ) , expandWhenExploded( false ) { } RingDiagram::Private::~Private() {} #define d d_func() RingDiagram::RingDiagram( QWidget* parent, PolarCoordinatePlane* plane ) : AbstractPieDiagram( new Private(), parent, plane ) { init(); } RingDiagram::~RingDiagram() { } void RingDiagram::init() { } /** * Creates an exact copy of this diagram. */ RingDiagram * RingDiagram::clone() const { return new RingDiagram( new Private( *d ) ); } bool RingDiagram::compare( const RingDiagram* other )const { if( other == this ) return true; if( ! other ){ return false; } /* qDebug() <<"\n RingDiagram::compare():"; // compare own properties qDebug() << (type() == other->type()); qDebug() << (relativeThickness() == other->relativeThickness()); qDebug() << (expandWhenExploded() == other->expandWhenExploded()); */ return // compare the base class ( static_cast(this)->compare( other ) ) && // compare own properties (relativeThickness() == other->relativeThickness()) && (expandWhenExploded() == other->expandWhenExploded()); } void RingDiagram::setRelativeThickness( bool relativeThickness ) { d->relativeThickness = relativeThickness; } bool RingDiagram::relativeThickness() const { return d->relativeThickness; } void RingDiagram::setExpandWhenExploded( bool expand ) { d->expandWhenExploded = expand; } bool RingDiagram::expandWhenExploded() const { return d->expandWhenExploded; } const QPair RingDiagram::calculateDataBoundaries () const { if ( !checkInvariants( true ) ) return QPair( QPointF( 0, 0 ), QPointF( 0, 0 ) ); const PieAttributes attrs( pieAttributes( model()->index( 0, 0, rootIndex() ) ) ); QPointF bottomLeft ( QPointF( 0, 0 ) ); QPointF topRight; // If we explode, we need extra space for the pie slice that has // the largest explosion distance. if ( attrs.explode() ) { const int rCount = rowCount(); const int colCount = columnCount(); qreal maxExplode = 0.0; for( int i = 0; i < rCount; ++i ){ qreal maxExplodeInThisRow = 0.0; for( int j = 0; j < colCount; ++j ){ const PieAttributes columnAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); //qDebug() << columnAttrs.explodeFactor(); maxExplodeInThisRow = qMax( maxExplodeInThisRow, columnAttrs.explodeFactor() ); } maxExplode += maxExplodeInThisRow; // FIXME: What if explode factor of inner ring is > 1.0 ? if ( !d->expandWhenExploded ) break; } // explode factor is relative to width (outer r - inner r) of one ring maxExplode /= ( rCount + 1); topRight = QPointF( 1.0+maxExplode, 1.0+maxExplode ); }else{ topRight = QPointF( 1.0, 1.0 ); } return QPair ( bottomLeft, topRight ); } void RingDiagram::paintEvent( QPaintEvent* ) { QPainter painter ( viewport() ); PaintContext ctx; ctx.setPainter ( &painter ); ctx.setRectangle( QRectF ( 0, 0, width(), height() ) ); paint ( &ctx ); } void RingDiagram::resizeEvent( QResizeEvent* ) { } static QRectF buildReferenceRect( const PolarCoordinatePlane* plane ) { QRectF contentsRect; QPointF referencePointAtTop = plane->translate( QPointF( 1, 0 ) ); QPointF temp = plane->translate( QPointF( 0, 0 ) ) - referencePointAtTop; const double offset = temp.y(); referencePointAtTop.setX( referencePointAtTop.x() - offset ); contentsRect.setTopLeft( referencePointAtTop ); contentsRect.setBottomRight( referencePointAtTop + QPointF( 2*offset, 2*offset) ); return contentsRect; } /* */ void RingDiagram::paint( PaintContext* ctx ) { // note: Not having any data model assigned is no bug // but we can not draw a diagram then either. if ( !checkInvariants(true) ) return; const PieAttributes attrs( pieAttributes() ); const int rCount = rowCount(); const int colCount = columnCount(); QRectF contentsRect( buildReferenceRect( polarCoordinatePlane() ) ); contentsRect = ctx->rectangle(); if( contentsRect.isEmpty() ) return; DataValueTextInfoList list; d->startAngles = QVector< QVector >( rCount, QVector( colCount ) ); d->angleLens = QVector< QVector >( rCount, QVector( colCount ) ); // compute position d->size = qMin( contentsRect.width(), contentsRect.height() ); // initial size // if the pies explode, we need to give them additional space => // make the basic size smaller qreal totalOffset = 0.0; for( int i = 0; i < rCount; ++i ){ qreal maxOffsetInThisRow = 0.0; for( int j = 0; j < colCount; ++j ){ const PieAttributes cellAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); //qDebug() << cellAttrs.explodeFactor(); const qreal explode = cellAttrs.explode() ? cellAttrs.explodeFactor() : 0.0; maxOffsetInThisRow = qMax( maxOffsetInThisRow, cellAttrs.gapFactor( false ) + explode ); } if ( !d->expandWhenExploded ) maxOffsetInThisRow -= (qreal)i; if ( maxOffsetInThisRow > 0.0 ) totalOffset += maxOffsetInThisRow; // FIXME: What if explode factor of inner ring is > 1.0 ? //if ( !d->expandWhenExploded ) // break; } // explode factor is relative to width (outer r - inner r) of one ring if ( rCount > 0 ) totalOffset /= ( rCount + 1 ); d->size /= ( 1.0 + totalOffset ); qreal x = ( contentsRect.width() == d->size ) ? 0.0 : ( ( contentsRect.width() - d->size ) / 2.0 ); qreal y = ( contentsRect.height() == d->size ) ? 0.0 : ( ( contentsRect.height() - d->size ) / 2.0 ); d->position = QRectF( x, y, d->size, d->size ); d->position.translate( contentsRect.left(), contentsRect.top() ); const PolarCoordinatePlane * plane = polarCoordinatePlane(); bool atLeastOneValue = false; // guard against completely empty tables QVariant vValY; d->clearListOfAlreadyDrawnDataValueTexts(); for ( int iRow = 0; iRow < rCount; ++iRow ) { const qreal sum = valueTotals( iRow ); if( sum == 0.0 ) //nothing to draw continue; qreal currentValue = plane ? plane->startPosition() : 0.0; const qreal sectorsPerValue = 360.0 / sum; for ( int iColumn = 0; iColumn < colCount; ++iColumn ) { // is there anything at all at this column? bool bOK; const double cellValue = qAbs( model()->data( model()->index( iRow, iColumn, rootIndex() ) ) .toDouble( &bOK ) ); if( bOK ){ d->startAngles[ iRow ][ iColumn ] = currentValue; d->angleLens[ iRow ][ iColumn ] = cellValue * sectorsPerValue; atLeastOneValue = true; } else { // mark as non-existent d->angleLens[ iRow ][ iColumn ] = 0.0; if ( iColumn > 0.0 ) d->startAngles[ iRow ][ iColumn ] = d->startAngles[ iRow ][ iColumn - 1 ]; else d->startAngles[ iRow ][ iColumn ] = currentValue; } //qDebug() << "d->startAngles["<angleLens["<angleLens[ iColumn ] // << " = " << d->startAngles[ iColumn ]+d->angleLens[ iColumn ]; currentValue = d->startAngles[ iRow ][ iColumn ] + d->angleLens[ iRow ][ iColumn ]; drawOnePie( ctx->painter(), iRow, iColumn, granularity() ); } } } #if defined ( Q_WS_WIN) #define trunc(x) ((int)(x)) #endif /** Internal method that draws one of the pies in a pie chart. \param painter the QPainter to draw in \param dataset the dataset to draw the pie for \param pie the pie to draw */ void RingDiagram::drawOnePie( QPainter* painter, uint dataset, uint pie, qreal granularity ) { // Is there anything to draw at all? const qreal angleLen = d->angleLens[ dataset ][ pie ]; if ( angleLen ) { const QModelIndex index( model()->index( dataset, pie, rootIndex() ) ); const PieAttributes attrs( pieAttributes( index ) ); drawPieSurface( painter, dataset, pie, granularity ); } } void RingDiagram::resize( const QSizeF& ) { } /** Internal method that draws the surface of one of the pies in a pie chart. \param painter the QPainter to draw in \param dataset the dataset to draw the pie for \param pie the pie to draw */ void RingDiagram::drawPieSurface( QPainter* painter, uint dataset, uint pie, qreal granularity ) { // Is there anything to draw at all? qreal angleLen = d->angleLens[ dataset ][ pie ]; if ( angleLen ) { qreal startAngle = d->startAngles[ dataset ][ pie ]; QModelIndex index( model()->index( dataset, pie, rootIndex() ) ); const PieAttributes attrs( pieAttributes( index ) ); const int rCount = rowCount(); const int colCount = columnCount(); int iPoint = 0; QRectF drawPosition = d->position;//piePosition( dataset, pie ); painter->setRenderHint ( QPainter::Antialiasing ); painter->setBrush( brush( index ) ); painter->setPen( pen( index ) ); // painter->setPen( pen ); //painter->setPen( Qt::red ); if ( angleLen == 360 ) { // full circle, avoid nasty line in the middle // FIXME: Draw a complete ring here //painter->drawEllipse( drawPosition ); } else { bool perfectMatch = false; qreal circularGap = 0.0; if ( attrs.gapFactor( true ) > 0.0 ) { // FIXME: Measure in degrees! circularGap = attrs.gapFactor( true ); //qDebug() << "gapFactor=" << attrs.gapFactor( false ); } QPolygonF poly; qreal degree = 0; qreal actualStartAngle = startAngle + circularGap; qreal actualAngleLen = angleLen - 2 * circularGap; qreal totalRadialExplode = 0.0; qreal maxRadialExplode = 0.0; qreal totalRadialGap = 0.0; qreal maxRadialGap = 0.0; for( uint i = rCount - 1; i > dataset; --i ){ qreal maxRadialExplodeInThisRow = 0.0; qreal maxRadialGapInThisRow = 0.0; for( int j = 0; j < colCount; ++j ){ const PieAttributes cellAttrs( pieAttributes( model()->index( i, j, rootIndex() ) ) ); //qDebug() << cellAttrs.explodeFactor(); if ( d->expandWhenExploded ) maxRadialGapInThisRow = qMax( maxRadialGapInThisRow, cellAttrs.gapFactor( false ) ); if ( !cellAttrs.explode() ) continue; // Don't use a gap for the very inner circle if ( d->expandWhenExploded ) maxRadialExplodeInThisRow = qMax( maxRadialExplodeInThisRow, cellAttrs.explodeFactor() ); } maxRadialExplode += maxRadialExplodeInThisRow; maxRadialGap += maxRadialGapInThisRow; // FIXME: What if explode factor of inner ring is > 1.0 ? //if ( !d->expandWhenExploded ) // break; } totalRadialGap = maxRadialGap + attrs.gapFactor( false ); totalRadialExplode = attrs.explode() ? maxRadialExplode + attrs.explodeFactor() : maxRadialExplode; while ( degree <= actualAngleLen ) { const QPointF p = pointOnCircle( drawPosition, dataset, pie, false, actualStartAngle + degree, totalRadialGap, totalRadialExplode ); poly.append( p ); degree += granularity; iPoint++; } if( ! perfectMatch ){ poly.append( pointOnCircle( drawPosition, dataset, pie, false, actualStartAngle + actualAngleLen, totalRadialGap, totalRadialExplode ) ); iPoint++; } // The center point of the inner brink const QPointF innerCenterPoint( poly[ int(iPoint / 2) ] ); actualStartAngle = startAngle + circularGap; actualAngleLen = angleLen - 2 * circularGap; degree = actualAngleLen; const int lastInnerBrinkPoint = iPoint; while ( degree >= 0 ){ poly.append( pointOnCircle( drawPosition, dataset, pie, true, actualStartAngle + degree, totalRadialGap, totalRadialExplode ) ); perfectMatch = (degree == 0); degree -= granularity; iPoint++; } // if necessary add one more point to fill the last small gap if( ! perfectMatch ){ poly.append( pointOnCircle( drawPosition, dataset, pie, true, actualStartAngle, totalRadialGap, totalRadialExplode ) ); iPoint++; } // The center point of the outer brink const QPointF outerCenterPoint( poly[ lastInnerBrinkPoint + int((iPoint - lastInnerBrinkPoint) / 2) ] ); //qDebug() << poly; //find the value and paint it //fix value position const qreal sum = valueTotals( dataset ); painter->drawPolygon( poly ); const QPointF centerPoint = (innerCenterPoint + outerCenterPoint) / 2.0; paintDataValueText( painter, index, centerPoint, angleLen*sum / 360 ); } } } /** * Auxiliary method returning a point to a given boundary * rectangle of the enclosed ellipse and an angle. */ QPointF RingDiagram::pointOnCircle( const QRectF& rect, int dataset, int pie, bool outer, qreal angle, qreal totalGapFactor, qreal totalExplodeFactor ) { qreal angleLen = d->angleLens[ dataset ][ pie ]; qreal startAngle = d->startAngles[ dataset ][ pie ]; QModelIndex index( model()->index( dataset, pie, rootIndex() ) ); const PieAttributes attrs( pieAttributes( index ) ); const int rCount = rowCount(); //const qreal gapFactor = attrs.gapFactor( false ); //qDebug() << "##" << attrs.explode(); //if ( attrs.explodeFactor() != 0.0 ) // qDebug() << attrs.explodeFactor(); qreal level = outer ? (rCount - dataset - 1) + 2 : (rCount - dataset - 1) + 1; //maxExplode /= rCount; //qDebug() << "dataset=" << dataset << "maxExplode=" << maxExplode; //level += maxExplode; const qreal offsetX = rCount > 0 ? level * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0; const qreal offsetY = rCount > 0 ? level * rect.height() / ( ( rCount + 1 ) * 2 ): 0.0; const qreal centerOffsetX = rCount > 0 ? totalExplodeFactor * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0; const qreal centerOffsetY = rCount > 0 ? totalExplodeFactor * rect.height() / ( ( rCount + 1 ) * 2 ): 0.0; const qreal gapOffsetX = rCount > 0 ? totalGapFactor * rect.width() / ( ( rCount + 1 ) * 2 ) : 0.0; const qreal gapOffsetY = rCount > 0 ? totalGapFactor * rect.height() / ( ( rCount + 1 ) * 2 ): 0.0; qreal explodeAngleRad = DEGTORAD( angle ); qreal cosAngle = cos( explodeAngleRad ); qreal sinAngle = -sin( explodeAngleRad ); qreal explodeAngleCenterRad = DEGTORAD( startAngle + angleLen / 2.0 ); qreal cosAngleCenter = cos( explodeAngleCenterRad ); qreal sinAngleCenter = -sin( explodeAngleCenterRad ); return QPointF( ( offsetX + gapOffsetX ) * cosAngle + centerOffsetX * cosAngleCenter + rect.center().x(), ( offsetY + gapOffsetY ) * sinAngle + centerOffsetY * sinAngleCenter + rect.center().y() ); } /*virtual*/ double RingDiagram::valueTotals() const { const int rCount = rowCount(); const int colCount = columnCount(); double total = 0.0; for ( int i = 0; i < rCount; ++i ) { for ( int j = 0; j < colCount; ++j ) { total += qAbs(model()->data( model()->index( 0, j, rootIndex() ) ).toDouble()); //qDebug() << model()->data( model()->index( 0, j, rootIndex() ) ).toDouble(); } } return total; } double RingDiagram::valueTotals( int dataset ) const { const int colCount = columnCount(); double total = 0.0; for ( int j = 0; j < colCount; ++j ) { total += qAbs(model()->data( model()->index( dataset, j, rootIndex() ) ).toDouble()); //qDebug() << model()->data( model()->index( 0, j, rootIndex() ) ).toDouble(); } return total; } /*virtual*/ double RingDiagram::numberOfValuesPerDataset() const { return model() ? model()->columnCount( rootIndex() ) : 0.0; } double RingDiagram::numberOfDatasets() const { return model() ? model()->rowCount( rootIndex() ) : 0.0; } /*virtual*/ double RingDiagram::numberOfGridRings() const { return 1; }