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