File indexing completed on 2024-12-15 04:02:29

0001 /*
0002  * SPDX-FileCopyrightText: 2001-2015 Klaralvdalens Datakonsult AB. All rights reserved.
0003  *
0004  * This file is part of the KD Chart library.
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "KChartCartesianAxis.h"
0010 #include "KChartCartesianAxis_p.h"
0011 
0012 #include <cmath>
0013 
0014 #include <QtDebug>
0015 #include <QPainter>
0016 #include <QPen>
0017 #include <QBrush>
0018 #include <QApplication>
0019 
0020 #include "KChartPaintContext.h"
0021 #include "KChartChart.h"
0022 #include "KChartAbstractCartesianDiagram.h"
0023 #include "KChartAbstractDiagram_p.h"
0024 #include "KChartAbstractGrid.h"
0025 #include "KChartPainterSaver_p.h"
0026 #include "KChartLayoutItems.h"
0027 #include "KChartBarDiagram.h"
0028 #include "KChartStockDiagram.h"
0029 #include "KChartLineDiagram.h"
0030 #include "KChartPrintingParameters.h"
0031 
0032 using namespace KChart;
0033 
0034 #define d (d_func())
0035 
0036 static qreal slightlyLessThan( qreal r )
0037 {
0038     if ( r == 0.0 ) {
0039         // scale down the epsilon somewhat arbitrarily
0040         return r - std::numeric_limits< qreal >::epsilon() * 1e-6;
0041     }
0042     // scale the epsilon so that it (hopefully) changes at least the least significant bit of r
0043     qreal diff = qAbs( r ) * std::numeric_limits< qreal >::epsilon() * 2.0;
0044     return r - diff;
0045 }
0046 
0047 static int numSignificantDecimalPlaces( qreal floatNumber )
0048 {
0049     static const int maxPlaces = 15;
0050     QString sample = QString::number( floatNumber, 'f', maxPlaces ).section( QLatin1Char('.'), 1,  2 );
0051     int ret = maxPlaces;
0052     for ( ; ret > 0; ret-- ) {
0053         if ( sample[ ret - 1 ] != QLatin1Char( '0' ) ) {
0054             break;
0055         }
0056     }
0057     return ret;
0058 }
0059 
0060 // Feature idea: In case of numeric labels, consider limiting the possible values of majorThinningFactor
0061 // to something like {1, 2, 5} * 10^n. Or even better, something that achieves round values in the
0062 // remaining labels.
0063 
0064 // ensure we take the const-overload of any following function, esp. required for strict iterators
0065 template<typename T>
0066 static const T& constify(T &v)
0067 {
0068     return v;
0069 }
0070 
0071 TickIterator::TickIterator( CartesianAxis* a, CartesianCoordinatePlane* plane, uint majorThinningFactor,
0072                             bool omitLastTick )
0073    : m_axis( a ),
0074      m_majorThinningFactor( majorThinningFactor ),
0075      m_majorLabelCount( 0 ),
0076      m_type( NoTick )
0077 {
0078     // deal with the things that are specific to axes (like annotations), before the generic init().
0079     const CartesianAxis::Private *axisPriv = CartesianAxis::Private::get( a );
0080     XySwitch xy( axisPriv->isVertical() );
0081     m_dimension = xy( plane->gridDimensionsList().first(), plane->gridDimensionsList().last() );
0082     if ( omitLastTick ) {
0083         // In bar and stock charts the last X tick is a fencepost with no associated value, which is
0084         // convenient for grid painting. Here we have to manually exclude it to avoid overpainting.
0085         m_dimension.end -= m_dimension.stepWidth;
0086     }
0087 
0088     m_annotations = axisPriv->annotations;
0089     m_customTicks = axisPriv->customTicksPositions;
0090 
0091     const qreal inf = std::numeric_limits< qreal >::infinity();
0092 
0093     if ( m_customTicks.count() ) {
0094         std::sort(m_customTicks.begin(), m_customTicks.end());
0095         m_customTickIndex = 0;
0096         m_customTick = m_customTicks.at( m_customTickIndex );
0097     } else {
0098         m_customTickIndex = -1;
0099         m_customTick = inf;
0100     }
0101 
0102     if ( m_majorThinningFactor > 1 && hasShorterLabels() ) {
0103         m_manualLabelTexts = m_axis->shortLabels();
0104     } else {
0105         m_manualLabelTexts = m_axis->labels();
0106     }
0107     m_manualLabelIndex = m_manualLabelTexts.isEmpty() ? -1 : 0;
0108 
0109     if ( !m_dimension.isCalculated ) {
0110         // ### depending on the data, it is difficult to impossible to choose anchors (where ticks
0111         //     corresponding to the header labels are) on the ordinate or even the abscissa with
0112         //     2-dimensional data. this should be somewhat mitigated by isCalculated only being false
0113         //     when header data labels should work, at least that seems to be what the code that sets up
0114         //     the dimensions is trying to do.
0115         QStringList dataHeaderLabels;
0116         AbstractDiagram* const dia = plane->diagram();
0117         dataHeaderLabels = dia->itemRowLabels();
0118         if ( !dataHeaderLabels.isEmpty() ) {
0119             AttributesModel* model = dia->attributesModel();
0120             const int anchorCount = model->rowCount( QModelIndex() );
0121             if ( anchorCount == dataHeaderLabels.count() ) {
0122                 for ( int i = 0; i < anchorCount; i++ ) {
0123                     // ### ordinal number as anchor point generally only works for 1-dimensional data
0124                     m_dataHeaderLabels.insert( qreal( i ), dataHeaderLabels.at( i ) );
0125                 }
0126             }
0127         }
0128     }
0129 
0130     bool hasMajorTicks = m_axis->rulerAttributes().showMajorTickMarks();
0131     bool hasMinorTicks = m_axis->rulerAttributes().showMinorTickMarks();
0132 
0133     init( xy.isY, hasMajorTicks, hasMinorTicks, plane );
0134 }
0135 
0136 static QMap< qreal, QString > allAxisAnnotations( const AbstractCoordinatePlane *plane, bool isY )
0137 {
0138     QMap< qreal, QString > annotations;
0139     const auto diagrams = plane->diagrams();
0140     for ( const AbstractDiagram* diagram : diagrams ) {
0141         const AbstractCartesianDiagram *cd = qobject_cast< const AbstractCartesianDiagram* >( diagram );
0142         if ( !cd ) {
0143             continue;
0144         }
0145         const auto axes = cd->axes();
0146         for ( const CartesianAxis* axis : axes ) {
0147             const CartesianAxis::Private *axisPriv = CartesianAxis::Private::get( axis );
0148             if ( axisPriv->isVertical() == isY ) {
0149                 annotations.insert( axisPriv->annotations );
0150             }
0151         }
0152     }
0153     return annotations;
0154 }
0155 
0156 TickIterator::TickIterator( bool isY, const DataDimension& dimension, bool useAnnotationsForTicks,
0157                             bool hasMajorTicks, bool hasMinorTicks, CartesianCoordinatePlane* plane )
0158    : m_axis( nullptr ),
0159      m_dimension( dimension ),
0160      m_majorThinningFactor( 1 ),
0161      m_majorLabelCount( 0 ),
0162      m_customTickIndex( -1 ),
0163      m_manualLabelIndex( -1 ),
0164      m_type( NoTick ),
0165      m_customTick( std::numeric_limits< qreal >::infinity() )
0166 {
0167     if ( useAnnotationsForTicks ) {
0168         m_annotations = allAxisAnnotations( plane, isY );
0169     }
0170     init( isY, hasMajorTicks, hasMinorTicks, plane );
0171 }
0172 
0173 void TickIterator::init( bool isY, bool hasMajorTicks, bool hasMinorTicks,
0174                          CartesianCoordinatePlane* plane )
0175 {
0176     Q_ASSERT( std::numeric_limits< qreal >::has_infinity );
0177 
0178     m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic;
0179     // sanity check against infinite loops
0180     hasMajorTicks = hasMajorTicks && ( m_dimension.stepWidth > 0 || m_isLogarithmic );
0181     hasMinorTicks = hasMinorTicks && ( m_dimension.subStepWidth > 0 || m_isLogarithmic );
0182 
0183     XySwitch xy( isY );
0184 
0185     GridAttributes gridAttributes = plane->gridAttributes( xy( Qt::Horizontal, Qt::Vertical ) );
0186     m_isLogarithmic = m_dimension.calcMode == AbstractCoordinatePlane::Logarithmic;
0187     if ( !m_isLogarithmic ) {
0188         // adjustedLowerUpperRange() is intended for use with linear scaling; specifically it would
0189         // round lower bounds < 1 to 0.
0190 
0191         const bool fixedRange = xy( plane->autoAdjustHorizontalRangeToData(),
0192                                     plane->autoAdjustVerticalRangeToData() ) >= 100;
0193         const bool adjustLower = gridAttributes.adjustLowerBoundToGrid() && !fixedRange;
0194         const bool adjustUpper = gridAttributes.adjustUpperBoundToGrid() && !fixedRange;
0195         m_dimension = AbstractGrid::adjustedLowerUpperRange( m_dimension, adjustLower, adjustUpper );
0196 
0197         m_decimalPlaces = numSignificantDecimalPlaces( m_dimension.stepWidth );
0198     } else {
0199         // the number of significant decimal places for each label naturally varies with logarithmic scaling
0200         m_decimalPlaces = -1;
0201     }
0202 
0203     const qreal inf = std::numeric_limits< qreal >::infinity();
0204 
0205     // try to place m_position just in front of the first tick to be drawn so that operator++()
0206     // can be used to find the first tick
0207     if ( m_isLogarithmic ) {
0208         if ( ISNAN( m_dimension.start ) || ISNAN( m_dimension.end ) ) {
0209             // this can happen in a spurious paint operation before everything is set up;
0210             // just bail out to avoid an infinite loop in that case.
0211             m_dimension.start = 0.0;
0212             m_dimension.end = 0.0;
0213             m_position = inf;
0214             m_majorTick = inf;
0215             m_minorTick = inf;
0216         } else if ( m_dimension.start >= 0 ) {
0217             m_position = m_dimension.start ? pow( 10.0, floor( log10( m_dimension.start ) ) - 1.0 )
0218                                            : 1e-6;
0219             m_majorTick = hasMajorTicks ? m_position : inf;
0220             m_minorTick = hasMinorTicks ? m_position * 20.0 : inf;
0221         } else {
0222             m_position = -pow( 10.0, ceil( log10( -m_dimension.start ) ) + 1.0 );
0223             m_majorTick = hasMajorTicks ? m_position : inf;
0224             m_minorTick = hasMinorTicks ? m_position * 0.09 : inf;
0225         }
0226     } else {
0227         m_majorTick = hasMajorTicks ? m_dimension.start : inf;
0228         m_minorTick = hasMinorTicks ? m_dimension.start : inf;
0229         m_position = slightlyLessThan( m_dimension.start );
0230     }
0231 
0232     ++( *this );
0233 }
0234 
0235 bool TickIterator::areAlmostEqual( qreal r1, qreal r2 ) const
0236 {
0237     if ( !m_isLogarithmic ) {
0238         qreal span = m_dimension.end - m_dimension.start;
0239         if ( span == 0 ) {
0240             // When start == end, we still want to show one tick if possible,
0241             // which needs this function to perform a reasonable comparison.
0242             span = qFuzzyIsNull( m_dimension.start) ? 1 : qAbs( m_dimension.start );
0243         }
0244         return qAbs( r2 - r1 ) < ( span ) * 1e-6;
0245     } else {
0246         return qAbs( r2 - r1 ) < qMax( qAbs( r1 ), qAbs( r2 ) ) * 0.01;
0247     }
0248 }
0249 
0250 bool TickIterator::isHigherPrecedence( qreal importantTick, qreal unimportantTick ) const
0251 {
0252     return importantTick != std::numeric_limits< qreal >::infinity() &&
0253            ( importantTick <= unimportantTick || areAlmostEqual( importantTick, unimportantTick ) );
0254 }
0255 
0256 void TickIterator::computeMajorTickLabel( int decimalPlaces )
0257 {
0258     if ( m_manualLabelIndex >= 0 ) {
0259         m_text = m_manualLabelTexts[ m_manualLabelIndex++ ];
0260         if ( m_manualLabelIndex >= m_manualLabelTexts.count() ) {
0261             // manual label texts repeat if there are less label texts than ticks on an axis
0262             m_manualLabelIndex = 0;
0263         }
0264         m_type = m_majorThinningFactor > 1 ? MajorTickManualShort : MajorTickManualLong;
0265     } else {
0266         // if m_axis is null, we are dealing with grid lines. grid lines never need labels.
0267         if ( m_axis && ( m_majorLabelCount++ % m_majorThinningFactor ) == 0 ) {
0268             QMap< qreal, QString >::ConstIterator it =
0269                 constify(m_dataHeaderLabels).lowerBound( slightlyLessThan( m_position ) );
0270 
0271             if ( it != m_dataHeaderLabels.constEnd() && areAlmostEqual( it.key(), m_position ) ) {
0272                 m_text = it.value();
0273                 m_type = MajorTickHeaderDataLabel;
0274             } else {
0275                 // 'f' to avoid exponential notation for large numbers, consistent with data value text
0276                 if ( decimalPlaces < 0 ) {
0277                     decimalPlaces = numSignificantDecimalPlaces( m_position );
0278                 }
0279                 m_text = QString::number( m_position, 'f', decimalPlaces );
0280                 m_type = MajorTick;
0281             }
0282         } else {
0283             m_text.clear();
0284             m_type = MajorTick;
0285         }
0286     }
0287 }
0288 
0289 void TickIterator::operator++()
0290 {
0291     if ( isAtEnd() ) {
0292         return;
0293     }
0294     const qreal inf = std::numeric_limits< qreal >::infinity();
0295 
0296     // make sure to find the next tick at a value strictly greater than m_position
0297 
0298     if ( !m_annotations.isEmpty() ) {
0299         QMap< qreal, QString >::ConstIterator it = constify(m_annotations).upperBound( m_position );
0300         if ( it != m_annotations.constEnd() ) {
0301             m_position = it.key();
0302             m_text = it.value();
0303             m_type = CustomTick;
0304         } else {
0305             m_position = inf;
0306         }
0307     } else if ( !m_isLogarithmic && m_dimension.stepWidth * 1e6 <
0308                                        qMax( qAbs( m_dimension.start ), qAbs( m_dimension.end ) ) ) {
0309         // If the step width is too small to increase m_position at all, we get an infinite loop.
0310         // This usually happens when m_dimension.start == m_dimension.end and both are very large.
0311         // When start == end, the step width defaults to 1, and it doesn't scale with start or end.
0312         // So currently, we bail and show no tick at all for empty ranges > 10^6, but at least we don't hang.
0313         m_position = inf;
0314     } else {
0315         // advance the calculated ticks
0316         if ( m_isLogarithmic ) {
0317             while ( m_majorTick <= m_position ) {
0318                 m_majorTick *= m_position >= 0 ? 10 : 0.1;
0319             }
0320             while ( m_minorTick <= m_position ) {
0321                 // the next major tick position should be greater than this
0322                 m_minorTick += m_majorTick * ( m_position >= 0 ? 0.1 : 1.0 );
0323             }
0324         } else {
0325             while ( m_majorTick <= m_position ) {
0326                 m_majorTick += m_dimension.stepWidth;
0327             }
0328             while ( m_minorTick <= m_position ) {
0329                 m_minorTick += m_dimension.subStepWidth;
0330             }
0331         }
0332 
0333         while ( m_customTickIndex >= 0 && m_customTick <= m_position ) {
0334             if ( ++m_customTickIndex >= m_customTicks.count() ) {
0335                 m_customTickIndex = -1;
0336                 m_customTick = inf;
0337                 break;
0338             }
0339             m_customTick = m_customTicks.at( m_customTickIndex );
0340         }
0341 
0342         // now see which kind of tick we'll have
0343         if ( isHigherPrecedence( m_customTick, m_majorTick ) && isHigherPrecedence( m_customTick, m_minorTick )  ) {
0344             m_position = m_customTick;
0345             computeMajorTickLabel( -1 );
0346             // override the MajorTick type here because those tick's labels are collision-tested, which we don't want
0347             // for custom ticks. they may be arbitrarily close to other ticks, causing excessive label thinning.
0348             if ( m_type == MajorTick ) {
0349                 m_type = CustomTick;
0350             }
0351         } else if ( isHigherPrecedence( m_majorTick, m_minorTick ) ) {
0352             m_position = m_majorTick;
0353             if ( m_minorTick != inf ) {
0354                 // realign minor to major
0355                 m_minorTick = m_majorTick;
0356             }
0357             computeMajorTickLabel( m_decimalPlaces );
0358        } else if ( m_minorTick != inf ) {
0359             m_position = m_minorTick;
0360             m_text.clear();
0361             m_type = MinorTick;
0362         } else {
0363             m_position = inf;
0364         }
0365     }
0366 
0367     if ( m_position > m_dimension.end || ISNAN( m_position ) ) {
0368         m_position = inf; // make isAtEnd() return true
0369         m_text.clear();
0370         m_type = NoTick;
0371     }
0372 }
0373 
0374 CartesianAxis::CartesianAxis( AbstractCartesianDiagram* diagram )
0375     : AbstractAxis ( new Private( diagram, this ), diagram )
0376 {
0377     init();
0378 }
0379 
0380 CartesianAxis::~CartesianAxis()
0381 {
0382     // when we remove the first axis it will unregister itself and
0383     // propagate the next one to the primary, thus the while loop
0384     while ( d->mDiagram ) {
0385         AbstractCartesianDiagram *cd = qobject_cast< AbstractCartesianDiagram* >( d->mDiagram );
0386         cd->takeAxis( this );
0387     }
0388     for ( AbstractDiagram *diagram : qAsConst(d->secondaryDiagrams) ) {
0389         AbstractCartesianDiagram *cd = qobject_cast< AbstractCartesianDiagram* >( diagram );
0390         cd->takeAxis( this );
0391     }
0392 }
0393 
0394 void CartesianAxis::init()
0395 {
0396     d->customTickLength = 3;
0397     d->position = Bottom;
0398     setCachedSizeDirty();
0399     connect( this, SIGNAL(coordinateSystemChanged()), SLOT(slotCoordinateSystemChanged()) );
0400 }
0401 
0402 
0403 bool CartesianAxis::compare( const CartesianAxis* other ) const
0404 {
0405     if ( other == this ) {
0406         return true;
0407     }
0408     if ( !other ) {
0409         return false;
0410     }
0411     return  AbstractAxis::compare( other ) && ( position() == other->position() ) &&
0412             ( titleText() == other->titleText() ) &&
0413             ( titleTextAttributes() == other->titleTextAttributes() );
0414 }
0415 
0416 void CartesianAxis::slotCoordinateSystemChanged()
0417 {
0418     layoutPlanes();
0419 }
0420 
0421 void CartesianAxis::setTitleText( const QString& text )
0422 {
0423     d->titleText = text;
0424     setCachedSizeDirty();
0425     layoutPlanes();
0426 }
0427 
0428 QString CartesianAxis::titleText() const
0429 {
0430     return d->titleText;
0431 }
0432 
0433 void CartesianAxis::setTitleTextAttributes( const TextAttributes &a )
0434 {
0435     d->titleTextAttributes = a;
0436     d->useDefaultTextAttributes = false;
0437     setCachedSizeDirty();
0438     layoutPlanes();
0439 }
0440 
0441 TextAttributes CartesianAxis::titleTextAttributes() const
0442 {
0443     if ( hasDefaultTitleTextAttributes() ) {
0444         TextAttributes ta( textAttributes() );
0445         Measure me( ta.fontSize() );
0446         me.setValue( me.value() * 1.5 );
0447         ta.setFontSize( me );
0448         return ta;
0449     }
0450     return d->titleTextAttributes;
0451 }
0452 
0453 void CartesianAxis::resetTitleTextAttributes()
0454 {
0455     d->useDefaultTextAttributes = true;
0456     setCachedSizeDirty();
0457     layoutPlanes();
0458 }
0459 
0460 bool CartesianAxis::hasDefaultTitleTextAttributes() const
0461 {
0462     return d->useDefaultTextAttributes;
0463 }
0464 
0465 void CartesianAxis::setPosition( Position p )
0466 {
0467     if ( d->position == p ) {
0468         return;
0469     }
0470     d->position = p;
0471     // Invalidating size is not always necessary if both old and new positions are horizontal or both
0472     // vertical, but in practice there could be small differences due to who-knows-what, so always adapt
0473     // to the possibly new size. Changing position is expensive anyway.
0474     setCachedSizeDirty();
0475     layoutPlanes();
0476 }
0477 
0478 #if defined(Q_COMPILER_MANGLES_RETURN_TYPE)
0479 const
0480 #endif
0481 CartesianAxis::Position CartesianAxis::position() const
0482 {
0483     return d->position;
0484 }
0485 
0486 void CartesianAxis::layoutPlanes()
0487 {
0488     if ( ! d->diagram() || ! d->diagram()->coordinatePlane() ) {
0489         return;
0490     }
0491     AbstractCoordinatePlane* plane = d->diagram()->coordinatePlane();
0492     if ( plane ) {
0493         plane->layoutPlanes();
0494     }
0495 }
0496 
0497 static bool referenceDiagramIsBarDiagram( const AbstractDiagram * diagram )
0498 {
0499     const AbstractCartesianDiagram * dia =
0500             qobject_cast< const AbstractCartesianDiagram * >( diagram );
0501     if ( dia && dia->referenceDiagram() )
0502         dia = dia->referenceDiagram();
0503     return qobject_cast< const BarDiagram* >( dia ) != nullptr;
0504 }
0505 
0506 static bool referenceDiagramNeedsCenteredAbscissaTicks( const AbstractDiagram *diagram )
0507 {
0508     const AbstractCartesianDiagram * dia =
0509             qobject_cast< const AbstractCartesianDiagram * >( diagram );
0510     if ( dia && dia->referenceDiagram() )
0511         dia = dia->referenceDiagram();
0512     if ( qobject_cast< const BarDiagram* >( dia ) )
0513         return true;
0514     if ( qobject_cast< const StockDiagram* >( dia ) )
0515         return true;
0516 
0517     const LineDiagram * lineDiagram = qobject_cast< const LineDiagram* >( dia );
0518     return lineDiagram && lineDiagram->centerDataPoints();
0519 }
0520 
0521 bool CartesianAxis::isAbscissa() const
0522 {
0523     const Qt::Orientation diagramOrientation = referenceDiagramIsBarDiagram( d->diagram() ) ? ( ( BarDiagram* )( d->diagram() ) )->orientation()
0524                                                                                             : Qt::Vertical;
0525     return diagramOrientation == Qt::Vertical ? position() == Bottom || position() == Top
0526                                               : position() == Left   || position() == Right;
0527 }
0528 
0529 bool CartesianAxis::isOrdinate() const
0530 {
0531     return !isAbscissa();
0532 }
0533 
0534 void CartesianAxis::paint( QPainter* painter )
0535 {
0536     if ( !d->diagram() || !d->diagram()->coordinatePlane() ) {
0537         return;
0538     }
0539     PaintContext ctx;
0540     ctx.setPainter ( painter );
0541     AbstractCoordinatePlane *const plane = d->diagram()->coordinatePlane();
0542     ctx.setCoordinatePlane( plane );
0543 
0544     ctx.setRectangle( QRectF( areaGeometry() ) );
0545     PainterSaver painterSaver( painter );
0546 
0547     // enable clipping only when required due to zoom, because it slows down painting
0548     // (the alternative to clipping when zoomed in requires much more work to paint just the right area)
0549     const qreal zoomFactor = d->isVertical() ? plane->zoomFactorY() : plane->zoomFactorX();
0550     if ( zoomFactor > 1.0 ) {
0551         painter->setClipRegion( areaGeometry().adjusted( - d->amountOfLeftOverlap - 1, - d->amountOfTopOverlap - 1,
0552                                                          d->amountOfRightOverlap + 1, d->amountOfBottomOverlap + 1 ) );
0553     }
0554     paintCtx( &ctx );
0555 }
0556 
0557 const TextAttributes CartesianAxis::Private::titleTextAttributesWithAdjustedRotation() const
0558 {
0559     TextAttributes titleTA( titleTextAttributes );
0560     int rotation = titleTA.rotation();
0561     if ( position == Left || position == Right ) {
0562         rotation += 270;
0563     }
0564     if ( rotation >= 360 ) {
0565         rotation -= 360;
0566     }
0567     // limit the allowed values to 0, 90, 180, 270
0568     rotation = ( rotation / 90 ) * 90;
0569     titleTA.setRotation( rotation );
0570     return titleTA;
0571 }
0572 
0573 QString CartesianAxis::Private::customizedLabelText( const QString& text, Qt::Orientation orientation,
0574                                                      qreal value ) const
0575 {
0576     // ### like in the old code, using int( value ) as column number...
0577     QString withUnits = diagram()->unitPrefix( int( value ), orientation, true ) +
0578                         text +
0579                         diagram()->unitSuffix( int( value ), orientation, true );
0580     return axis()->customizedLabel( withUnits );
0581 }
0582 
0583 void CartesianAxis::setTitleSpace( qreal axisTitleSpace )
0584 {
0585     d->axisTitleSpace = axisTitleSpace;
0586 }
0587 
0588 qreal CartesianAxis::titleSpace() const
0589 {
0590     return d->axisTitleSpace;
0591 }
0592 
0593 void CartesianAxis::setTitleSize( qreal value )
0594 {
0595     Q_UNUSED( value )
0596     // ### remove me
0597 }
0598 
0599 qreal CartesianAxis::titleSize() const
0600 {
0601     // ### remove me
0602     return 1.0;
0603 }
0604 
0605 void CartesianAxis::Private::drawTitleText( QPainter* painter, CartesianCoordinatePlane* plane,
0606                                             const QRect& geoRect ) const
0607 {
0608     const TextAttributes titleTA( titleTextAttributesWithAdjustedRotation() );
0609     if ( titleTA.isVisible() ) {
0610         TextLayoutItem titleItem( titleText, titleTA, plane->parent(), KChartEnums::MeasureOrientationMinimum,
0611                                   Qt::AlignHCenter | Qt::AlignVCenter );
0612         QPointF point;
0613         QSize size = titleItem.sizeHint();
0614         switch ( position ) {
0615         case Top:
0616             point.setX( geoRect.left() + geoRect.width() / 2 );
0617             point.setY( geoRect.top() + ( size.height() / 2 ) / axisTitleSpace );
0618             size.setWidth( qMin( size.width(), axis()->geometry().width() ) );
0619             break;
0620         case Bottom:
0621             point.setX( geoRect.left() + geoRect.width() / 2 );
0622             point.setY( geoRect.bottom() - ( size.height() / 2 ) / axisTitleSpace );
0623             size.setWidth( qMin( size.width(), axis()->geometry().width() ) );
0624             break;
0625         case Left:
0626             point.setX( geoRect.left() + ( size.width() / 2 ) / axisTitleSpace );
0627             point.setY( geoRect.top() + geoRect.height() / 2 );
0628             size.setHeight( qMin( size.height(), axis()->geometry().height() ) );
0629             break;
0630         case Right:
0631             point.setX( geoRect.right() - ( size.width() / 2 ) / axisTitleSpace );
0632             point.setY( geoRect.top() + geoRect.height() / 2 );
0633             size.setHeight( qMin( size.height(), axis()->geometry().height() ) );
0634             break;
0635         }
0636         const PainterSaver painterSaver( painter );
0637         painter->setClipping( false );
0638         painter->translate( point );
0639         titleItem.setGeometry( QRect( QPoint( -size.width() / 2, -size.height() / 2 ), size ) );
0640         titleItem.paint( painter );
0641     }
0642 }
0643 
0644 bool CartesianAxis::Private::isVertical() const
0645 {
0646     return axis()->isAbscissa() == AbstractDiagram::Private::get( diagram() )->isTransposed();
0647 }
0648 
0649 void CartesianAxis::paintCtx( PaintContext* context )
0650 {
0651     Q_ASSERT_X ( d->diagram(), "CartesianAxis::paint",
0652                  "Function call not allowed: The axis is not assigned to any diagram." );
0653 
0654     CartesianCoordinatePlane* plane = dynamic_cast<CartesianCoordinatePlane*>( context->coordinatePlane() );
0655     Q_ASSERT_X ( plane, "CartesianAxis::paint",
0656                  "Bad function call: PaintContext::coordinatePlane() NOT a cartesian plane." );
0657 
0658     // note: Not having any data model assigned is no bug
0659     //       but we can not draw an axis then either.
0660     if ( !d->diagram()->model() ) {
0661         return;
0662     }
0663 
0664     const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks( d->diagram() ) && isAbscissa();
0665 
0666     XySwitch geoXy( d->isVertical() );
0667 
0668     QPainter* const painter = context->painter();
0669 
0670     // determine the position of the axis (also required for labels) and paint it
0671 
0672     qreal transversePosition = signalingNaN; // in data space
0673     // the next one describes an additional shift in screen space; it is unfortunately required to
0674     // make axis sharing work, which uses the areaGeometry() to override the position of the axis.
0675     qreal transverseScreenSpaceShift = signalingNaN;
0676     {
0677         // determine the unadulterated position in screen space
0678 
0679         DataDimension dimX = plane->gridDimensionsList().first();
0680         DataDimension dimY = plane->gridDimensionsList().last();
0681         QPointF start( dimX.start, dimY.start );
0682         QPointF end( dimX.end, dimY.end );
0683         // consider this: you can turn a diagonal line into a horizontal or vertical line on any
0684         // edge by changing just one of its four coordinates.
0685         switch ( position() ) {
0686         case CartesianAxis::Bottom:
0687             end.setY( dimY.start );
0688             break;
0689         case CartesianAxis::Top:
0690             start.setY( dimY.end );
0691             break;
0692         case CartesianAxis::Left:
0693             end.setX( dimX.start );
0694             break;
0695         case CartesianAxis::Right:
0696             start.setX( dimX.end );
0697             break;
0698         }
0699 
0700         transversePosition = geoXy( start.y(), start.x() );
0701 
0702         QPointF transStart = plane->translate( start );
0703         QPointF transEnd = plane->translate( end );
0704 
0705         // an externally set areaGeometry() moves the axis position transversally; the shift is
0706         // nonzero only when this is a shared axis
0707 
0708         const QRect geo = areaGeometry();
0709         switch ( position() ) {
0710         case CartesianAxis::Bottom:
0711             transverseScreenSpaceShift = geo.top() - transStart.y();
0712             break;
0713         case CartesianAxis::Top:
0714             transverseScreenSpaceShift = geo.bottom() - transStart.y();
0715             break;
0716         case CartesianAxis::Left:
0717             transverseScreenSpaceShift = geo.right() - transStart.x();
0718             break;
0719         case CartesianAxis::Right:
0720             transverseScreenSpaceShift = geo.left() - transStart.x();
0721             break;
0722         }
0723 
0724         geoXy.lvalue( transStart.ry(), transStart.rx() ) += transverseScreenSpaceShift;
0725         geoXy.lvalue( transEnd.ry(), transEnd.rx() ) += transverseScreenSpaceShift;
0726 
0727         if ( rulerAttributes().showRulerLine() ) {
0728             painter->save();
0729             painter->setClipping( false );
0730             painter->setPen(rulerAttributes().rulerLinePen());
0731             painter->drawLine( transStart, transEnd );
0732             painter->restore();
0733         }
0734     }
0735 
0736     // paint ticks and labels
0737 
0738     TextAttributes labelTA = textAttributes();
0739     RulerAttributes rulerAttr = rulerAttributes();
0740 
0741     int labelThinningFactor = 1;
0742     // TODO: label thinning also when grid line distance < 4 pixels, not only when labels collide
0743     TextLayoutItem *tickLabel = new TextLayoutItem( QString(), labelTA, plane->parent(),
0744                                                     KChartEnums::MeasureOrientationMinimum, Qt::AlignLeft );
0745     TextLayoutItem *prevTickLabel = new TextLayoutItem( QString(), labelTA, plane->parent(),
0746                                                         KChartEnums::MeasureOrientationMinimum, Qt::AlignLeft );
0747     QPointF prevTickLabelPos;
0748     enum {
0749         Layout = 0,
0750         Painting,
0751         Done
0752     };
0753     for ( int step = labelTA.isVisible() ? Layout : Painting; step < Done; step++ ) {
0754         bool skipFirstTick = !rulerAttr.showFirstTick();
0755         bool isFirstLabel = true;
0756         for ( TickIterator it( this, plane, labelThinningFactor, centerTicks ); !it.isAtEnd(); ++it ) {
0757             if ( skipFirstTick ) {
0758                 skipFirstTick = false;
0759                 continue;
0760             }
0761 
0762             const qreal drawPos = it.position() + ( centerTicks ? 0.5 : 0. );
0763             QPointF onAxis = plane->translate( geoXy( QPointF( drawPos, transversePosition ) ,
0764                                                       QPointF( transversePosition, drawPos ) ) );
0765             geoXy.lvalue( onAxis.ry(), onAxis.rx() ) += transverseScreenSpaceShift;
0766             const bool isOutwardsPositive = position() == Bottom || position() == Right;
0767 
0768             // paint the tick mark
0769 
0770             QPointF tickEnd = onAxis;
0771             qreal tickLen = it.type() == TickIterator::CustomTick ?
0772                             d->customTickLength : tickLength( it.type() == TickIterator::MinorTick );
0773             geoXy.lvalue( tickEnd.ry(), tickEnd.rx() ) += isOutwardsPositive ? tickLen : -tickLen;
0774 
0775             // those adjustments are required to paint the ticks exactly on the axis and of the right length
0776             if ( position() == Top ) {
0777                 onAxis.ry() += 1;
0778                 tickEnd.ry() += 1;
0779             } else if ( position() == Left ) {
0780                 tickEnd.rx() += 1;
0781             }
0782 
0783             if ( step == Painting ) {
0784                 painter->save();
0785                 if ( rulerAttr.hasTickMarkPenAt( it.position() ) ) {
0786                     painter->setPen( rulerAttr.tickMarkPen( it.position() ) );
0787                 } else {
0788                     painter->setPen( it.type() == TickIterator::MinorTick ? rulerAttr.minorTickMarkPen()
0789                                                                           : rulerAttr.majorTickMarkPen() );
0790                 }
0791                 painter->drawLine( onAxis, tickEnd );
0792                 painter->restore();
0793             }
0794 
0795             if ( it.text().isEmpty() || !labelTA.isVisible() ) {
0796                 // the following code in the loop is only label painting, so skip it
0797                 continue;
0798             }
0799 
0800             // paint the label
0801 
0802             QString text = it.text();
0803             if ( it.type() == TickIterator::MajorTick ) {
0804                 // add unit prefixes and suffixes, then customize
0805                 text = d->customizedLabelText( text, geoXy( Qt::Horizontal, Qt::Vertical ), it.position() );
0806             } else if ( it.type() == TickIterator::MajorTickHeaderDataLabel ) {
0807                 // unit prefixes and suffixes have already been added in this case - only customize
0808                 text = customizedLabel( text );
0809             }
0810 
0811             tickLabel->setText( text );
0812             QSizeF size = QSizeF( tickLabel->sizeHint() );
0813             QPolygon labelPoly = tickLabel->boundingPolygon();
0814             Q_ASSERT( labelPoly.count() == 4 );
0815 
0816             // for alignment, find the label polygon edge "most parallel" and closest to the axis
0817 
0818             int axisAngle = 0;
0819             switch ( position() ) {
0820             case Bottom:
0821                 axisAngle = 0; break;
0822             case Top:
0823                 axisAngle = 180; break;
0824             case Right:
0825                 axisAngle = 270; break;
0826             case Left:
0827                 axisAngle = 90; break;
0828             default:
0829                 Q_ASSERT( false );
0830             }
0831             // the left axis is not actually pointing down and the top axis not actually pointing
0832             // left, but their corresponding closest edges of a rectangular unrotated label polygon are.
0833 
0834             int relAngle = axisAngle - labelTA.rotation() + 45;
0835             if ( relAngle < 0 ) {
0836                 relAngle += 360;
0837             }
0838             int polyCorner1 = relAngle / 90;
0839             QPoint p1 = labelPoly.at( polyCorner1 );
0840             QPoint p2 = labelPoly.at( polyCorner1 == 3 ? 0 : ( polyCorner1 + 1 ) );
0841 
0842             QPointF labelPos = tickEnd;
0843 
0844             qreal labelMargin = rulerAttr.labelMargin();
0845             if ( labelMargin < 0 ) {
0846                 labelMargin = QFontMetricsF( tickLabel->realFont() ).height() * 0.5;
0847             }
0848             labelMargin -= tickLabel->marginWidth(); // make up for the margin that's already there
0849 
0850             switch ( position() ) {
0851             case Left:
0852                 labelPos += QPointF( -size.width() - labelMargin,
0853                                      -0.45 * size.height() - 0.5 * ( p1.y() + p2.y() ) );
0854                 break;
0855             case Right:
0856                 labelPos += QPointF( labelMargin,
0857                                      -0.45 * size.height() - 0.5 * ( p1.y() + p2.y() ) );
0858                 break;
0859             case Top:
0860                 labelPos += QPointF( -0.45 * size.width() - 0.5 * ( p1.x() + p2.x() ),
0861                                      -size.height() - labelMargin );
0862                 break;
0863             case Bottom:
0864                 labelPos += QPointF( -0.45 * size.width() - 0.5 * ( p1.x() + p2.x() ),
0865                                      labelMargin );
0866                 break;
0867             }
0868 
0869             tickLabel->setGeometry( QRect( labelPos.toPoint(), size.toSize() ) );
0870 
0871             if ( step == Painting ) {
0872                 tickLabel->paint( painter );
0873             }
0874 
0875             // collision check the current label against the previous one
0876 
0877             // like in the old code, we don't shorten or decimate labels if they are already the
0878             // manual short type, or if they are the manual long type and on the vertical axis
0879             // ### they can still collide though, especially when they're rotated!
0880             if ( step == Layout ) {
0881                 int spaceSavingRotation = geoXy( 270, 0 );
0882                 bool canRotate = labelTA.autoRotate() && labelTA.rotation() != spaceSavingRotation;
0883                 const bool canShortenLabels = !geoXy.isY && it.type() == TickIterator::MajorTickManualLong &&
0884                                               it.hasShorterLabels();
0885                 bool collides = false;
0886                 if ( it.type() == TickIterator::MajorTick || it.type() == TickIterator::MajorTickHeaderDataLabel
0887                      || canShortenLabels || canRotate ) {
0888                     if ( isFirstLabel ) {
0889                         isFirstLabel = false;
0890                     } else {
0891                         collides = tickLabel->intersects( *prevTickLabel, labelPos, prevTickLabelPos );
0892                         qSwap( prevTickLabel, tickLabel );
0893                     }
0894                     prevTickLabelPos = labelPos;
0895                 }
0896                 if ( collides ) {
0897                     // to make room, we try in order: shorten, rotate, decimate
0898                     if ( canRotate && !canShortenLabels ) {
0899                         labelTA.setRotation( spaceSavingRotation );
0900                         // tickLabel will be reused in the next round
0901                         tickLabel->setTextAttributes( labelTA );
0902                     } else {
0903                         labelThinningFactor++;
0904                     }
0905                     step--; // relayout
0906                     break;
0907                 }
0908             }
0909         }
0910     }
0911     delete tickLabel;
0912     tickLabel = nullptr;
0913     delete prevTickLabel;
0914     prevTickLabel = nullptr;
0915 
0916     if ( ! titleText().isEmpty() ) {
0917         d->drawTitleText( painter, plane, geometry() );
0918     }
0919 }
0920 
0921 /* pure virtual in QLayoutItem */
0922 bool CartesianAxis::isEmpty() const
0923 {
0924     return false; // if the axis exists, it has some (perhaps default) content
0925 }
0926 
0927 /* pure virtual in QLayoutItem */
0928 Qt::Orientations CartesianAxis::expandingDirections() const
0929 {
0930     Qt::Orientations ret;
0931     switch ( position() ) {
0932     case Bottom:
0933     case Top:
0934         ret = Qt::Horizontal;
0935         break;
0936     case Left:
0937     case Right:
0938         ret = Qt::Vertical;
0939         break;
0940     default:
0941         Q_ASSERT( false );
0942         break;
0943     };
0944     return ret;
0945 }
0946 
0947 void CartesianAxis::setCachedSizeDirty() const
0948 {
0949     d->cachedMaximumSize = QSize();
0950 }
0951 
0952 /* pure virtual in QLayoutItem */
0953 QSize CartesianAxis::maximumSize() const
0954 {
0955     if ( ! d->cachedMaximumSize.isValid() )
0956         d->cachedMaximumSize = d->calculateMaximumSize();
0957     return d->cachedMaximumSize;
0958 }
0959 
0960 QSize CartesianAxis::Private::calculateMaximumSize() const
0961 {
0962     if ( !diagram() ) {
0963         return QSize();
0964     }
0965 
0966     CartesianCoordinatePlane* plane = dynamic_cast< CartesianCoordinatePlane* >( diagram()->coordinatePlane() );
0967     Q_ASSERT( plane );
0968     QObject* refArea = plane->parent();
0969     const bool centerTicks = referenceDiagramNeedsCenteredAbscissaTicks( diagram() )
0970                              && axis()->isAbscissa();
0971 
0972     // we ignore:
0973     // - label thinning (expensive, not worst case and we want worst case)
0974     // - label autorotation (expensive, obscure feature(?))
0975     // - axis length (it is determined by the plane / diagram / chart anyway)
0976     // - the title's influence on axis length; this one might be TODO. See KDCH-863.
0977 
0978     XySwitch geoXy( isVertical() );
0979     qreal size = 0; // this is the size transverse to the axis direction
0980 
0981     // the following variables describe how much the first and last label stick out over the axis
0982     // area, so that the geometry of surrounding layout items can be adjusted to make room.
0983     qreal startOverhang = 0.0;
0984     qreal endOverhang = 0.0;
0985 
0986     if ( mAxis->textAttributes().isVisible() ) {
0987         // these four are used just to calculate startOverhang and endOverhang
0988         qreal lowestLabelPosition = signalingNaN;
0989         qreal highestLabelPosition = signalingNaN;
0990         qreal lowestLabelLongitudinalSize = signalingNaN;
0991         qreal highestLabelLongitudinalSize = signalingNaN;
0992 
0993         TextLayoutItem tickLabel( QString(), mAxis->textAttributes(), refArea,
0994                                   KChartEnums::MeasureOrientationMinimum, Qt::AlignLeft );
0995         const RulerAttributes rulerAttr = mAxis->rulerAttributes();
0996 
0997         bool showFirstTick = rulerAttr.showFirstTick();
0998         for ( TickIterator it( axis(), plane, 1, centerTicks ); !it.isAtEnd(); ++it ) {
0999             const qreal drawPos = it.position() + ( centerTicks ? 0.5 : 0. );
1000             if ( !showFirstTick ) {
1001                 showFirstTick = true;
1002                 continue;
1003             }
1004 
1005             qreal labelSizeTransverse = 0.0;
1006             qreal labelMargin = 0.0;
1007             QString text = it.text();
1008             if ( !text.isEmpty() ) {
1009                 QPointF labelPosition = plane->translate( QPointF( geoXy( drawPos, qreal(1.0) ),
1010                                                                    geoXy( qreal(1.0), drawPos ) ) );
1011                 highestLabelPosition = geoXy( labelPosition.x(), labelPosition.y() );
1012 
1013                 if ( it.type() == TickIterator::MajorTick ) {
1014                     // add unit prefixes and suffixes, then customize
1015                     text = customizedLabelText( text, geoXy( Qt::Horizontal, Qt::Vertical ), it.position() );
1016                 } else if ( it.type() == TickIterator::MajorTickHeaderDataLabel ) {
1017                     // unit prefixes and suffixes have already been added in this case - only customize
1018                     text = axis()->customizedLabel( text );
1019                 }
1020                 tickLabel.setText( text );
1021 
1022                 QSize sz = tickLabel.sizeHint();
1023                 highestLabelLongitudinalSize = geoXy( sz.width(), sz.height() );
1024                 if ( ISNAN( lowestLabelLongitudinalSize ) ) {
1025                     lowestLabelLongitudinalSize = highestLabelLongitudinalSize;
1026                     lowestLabelPosition = highestLabelPosition;
1027                 }
1028 
1029                 labelSizeTransverse = geoXy( sz.height(), sz.width() );
1030                 labelMargin = rulerAttr.labelMargin();
1031                 if ( labelMargin < 0 ) {
1032                     labelMargin = QFontMetricsF( tickLabel.realFont() ).height() * 0.5;
1033                 }
1034                 labelMargin -= tickLabel.marginWidth(); // make up for the margin that's already there
1035             }
1036             qreal tickLength = it.type() == TickIterator::CustomTick ?
1037                                customTickLength : axis()->tickLength( it.type() == TickIterator::MinorTick );
1038             size = qMax( size, tickLength + labelMargin + labelSizeTransverse );
1039         }
1040 
1041         const DataDimension dimX = plane->gridDimensionsList().first();
1042         const DataDimension dimY = plane->gridDimensionsList().last();
1043 
1044         QPointF pt = plane->translate( QPointF( dimX.start, dimY.start ) );
1045         const qreal lowestPosition = geoXy( pt.x(), pt.y() );
1046         pt = plane->translate( QPointF( dimX.end, dimY.end ) );
1047         const qreal highestPosition = geoXy( pt.x(), pt.y() );
1048 
1049         // the geoXy( 1.0, -1.0 ) here is necessary because Qt's y coordinate is inverted
1050         startOverhang = qMax( 0.0, ( lowestPosition - lowestLabelPosition ) * geoXy( 1.0, -1.0 ) +
1051                                      lowestLabelLongitudinalSize * 0.5 );
1052         endOverhang = qMax( 0.0, ( highestLabelPosition - highestPosition ) * geoXy( 1.0, -1.0 ) +
1053                                    highestLabelLongitudinalSize * 0.5 );
1054     }
1055 
1056     amountOfLeftOverlap = geoXy( startOverhang, qreal(0.0) );
1057     amountOfRightOverlap = geoXy( endOverhang, qreal(0.0) );
1058     amountOfBottomOverlap = geoXy( qreal(0.0), startOverhang );
1059     amountOfTopOverlap = geoXy( qreal(0.0), endOverhang );
1060 
1061     const TextAttributes titleTA = titleTextAttributesWithAdjustedRotation();
1062     if ( titleTA.isVisible() && !axis()->titleText().isEmpty() ) {
1063         TextLayoutItem title( axis()->titleText(), titleTA, refArea, KChartEnums::MeasureOrientationMinimum,
1064                               Qt::AlignHCenter | Qt::AlignVCenter );
1065 
1066         QFontMetricsF titleFM( title.realFont(), GlobalMeasureScaling::paintDevice() );
1067         size += geoXy( titleFM.height() * 0.33, titleFM.averageCharWidth() * 0.55 ); // spacing
1068         size += geoXy( title.sizeHint().height(), title.sizeHint().width() );
1069     }
1070 
1071     // the size parallel to the axis direction is not determined by us, so we just return 1
1072     return QSize( geoXy( 1, int( size ) ), geoXy( int ( size ), 1 ) );
1073 }
1074 
1075 /* pure virtual in QLayoutItem */
1076 QSize CartesianAxis::minimumSize() const
1077 {
1078     return maximumSize();
1079 }
1080 
1081 /* pure virtual in QLayoutItem */
1082 QSize CartesianAxis::sizeHint() const
1083 {
1084     return maximumSize();
1085 }
1086 
1087 /* pure virtual in QLayoutItem */
1088 void CartesianAxis::setGeometry( const QRect& r )
1089 {
1090     if ( d->geometry != r ) {
1091         d->geometry = r;
1092         setCachedSizeDirty();
1093     }
1094 }
1095 
1096 /* pure virtual in QLayoutItem */
1097 QRect CartesianAxis::geometry() const
1098 {
1099     return d->geometry;
1100 }
1101 
1102 void CartesianAxis::setCustomTickLength( int value )
1103 {
1104     if ( d->customTickLength == value ) {
1105         return;
1106     }
1107     d->customTickLength = value;
1108     setCachedSizeDirty();
1109     layoutPlanes();
1110 }
1111 
1112 int CartesianAxis::customTickLength() const
1113 {
1114     return d->customTickLength;
1115 }
1116 
1117 int CartesianAxis::tickLength( bool subUnitTicks ) const
1118 {
1119     const RulerAttributes& rulerAttr = rulerAttributes();
1120     return subUnitTicks ? rulerAttr.minorTickMarkLength() : rulerAttr.majorTickMarkLength();
1121 }
1122 
1123 QMap< qreal, QString > CartesianAxis::annotations() const
1124 {
1125     return d->annotations;
1126 }
1127 
1128 void CartesianAxis::setAnnotations( const QMap< qreal, QString >& annotations )
1129 {
1130     if ( d->annotations == annotations )
1131         return;
1132 
1133     d->annotations = annotations;
1134     setCachedSizeDirty();
1135     layoutPlanes();
1136 }
1137 
1138 QList< qreal > CartesianAxis::customTicks() const
1139 {
1140     return d->customTicksPositions;
1141 }
1142 
1143 void CartesianAxis::setCustomTicks( const QList< qreal >& customTicksPositions )
1144 {
1145     if ( d->customTicksPositions == customTicksPositions )
1146         return;
1147 
1148     d->customTicksPositions = customTicksPositions;
1149     setCachedSizeDirty();
1150     layoutPlanes();
1151 }
1152 
1153 #if !defined(QT_NO_DEBUG_STREAM)
1154 QDebug operator<<(QDebug dbg, KChart::CartesianAxis::Position pos)
1155 {
1156     switch (pos) {
1157         case KChart::CartesianAxis::Bottom: dbg << "KChart::CartesianAxis::Bottom"; break;
1158         case KChart::CartesianAxis::Top: dbg << "KChart::CartesianAxis::Top"; break;
1159         case KChart::CartesianAxis::Left: dbg << "KChart::CartesianAxis::Left"; break;
1160         case KChart::CartesianAxis::Right: dbg << "KChart::CartesianAxis::Right"; break;
1161         default: dbg << "KChart::CartesianAxis::Invalid"; break;
1162     }
1163     return dbg;
1164 }
1165 #endif