File indexing completed on 2024-12-15 04:02:31
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 "KChartCartesianGrid.h" 0010 #include "KChartAbstractCartesianDiagram.h" 0011 #include "KChartPaintContext.h" 0012 #include "KChartPainterSaver_p.h" 0013 #include "KChartPrintingParameters.h" 0014 #include "KChartFrameAttributes.h" 0015 #include "KChartCartesianAxis_p.h" 0016 #include "KChartMath_p.h" 0017 0018 #include <QPainter> 0019 #include <QPainterPath> 0020 0021 using namespace KChart; 0022 0023 CartesianGrid::CartesianGrid() 0024 : AbstractGrid(), m_minsteps( 2 ), m_maxsteps( 12 ) 0025 { 0026 } 0027 0028 CartesianGrid::~CartesianGrid() 0029 { 0030 } 0031 0032 int CartesianGrid::minimalSteps() const 0033 { 0034 return m_minsteps; 0035 } 0036 0037 void CartesianGrid::setMinimalSteps(int minsteps) 0038 { 0039 m_minsteps = minsteps; 0040 } 0041 0042 int CartesianGrid::maximalSteps() const 0043 { 0044 return m_maxsteps; 0045 } 0046 0047 void CartesianGrid::setMaximalSteps(int maxsteps) 0048 { 0049 m_maxsteps = maxsteps; 0050 } 0051 0052 void CartesianGrid::drawGrid( PaintContext* context ) 0053 { 0054 CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( context->coordinatePlane() ); 0055 const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) ); 0056 const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) ); 0057 if ( !gridAttrsX.isGridVisible() && !gridAttrsX.isSubGridVisible() && 0058 !gridAttrsY.isGridVisible() && !gridAttrsY.isSubGridVisible() ) { 0059 return; 0060 } 0061 // This plane is used for translating the coordinates - not for the data boundaries 0062 QPainter *p = context->painter(); 0063 PainterSaver painterSaver( p ); 0064 // sharedAxisMasterPlane() changes the painter's coordinate transformation(!) 0065 plane = qobject_cast< CartesianCoordinatePlane* >( plane->sharedAxisMasterPlane( context->painter() ) ); 0066 Q_ASSERT_X ( plane, "CartesianGrid::drawGrid", 0067 "Bad function call: PaintContext::coodinatePlane() NOT a cartesian plane." ); 0068 0069 // update the calculated mDataDimensions before using them 0070 updateData( context->coordinatePlane() ); // this, in turn, calls our calculateGrid(). 0071 Q_ASSERT_X ( mDataDimensions.count() == 2, "CartesianGrid::drawGrid", 0072 "Error: updateData did not return exactly two dimensions." ); 0073 if ( !isBoundariesValid( mDataDimensions ) ) { 0074 return; 0075 } 0076 0077 const DataDimension dimX = mDataDimensions.first(); 0078 const DataDimension dimY = mDataDimensions.last(); 0079 const bool isLogarithmicX = dimX.calcMode == AbstractCoordinatePlane::Logarithmic; 0080 const bool isLogarithmicY = dimY.calcMode == AbstractCoordinatePlane::Logarithmic; 0081 0082 qreal minValueX = qMin( dimX.start, dimX.end ); 0083 qreal maxValueX = qMax( dimX.start, dimX.end ); 0084 qreal minValueY = qMin( dimY.start, dimY.end ); 0085 qreal maxValueY = qMax( dimY.start, dimY.end ); 0086 { 0087 bool adjustXLower = !isLogarithmicX && gridAttrsX.adjustLowerBoundToGrid(); 0088 bool adjustXUpper = !isLogarithmicX && gridAttrsX.adjustUpperBoundToGrid(); 0089 bool adjustYLower = !isLogarithmicY && gridAttrsY.adjustLowerBoundToGrid(); 0090 bool adjustYUpper = !isLogarithmicY && gridAttrsY.adjustUpperBoundToGrid(); 0091 AbstractGrid::adjustLowerUpperRange( minValueX, maxValueX, dimX.stepWidth, adjustXLower, adjustXUpper ); 0092 AbstractGrid::adjustLowerUpperRange( minValueY, maxValueY, dimY.stepWidth, adjustYLower, adjustYUpper ); 0093 } 0094 0095 if ( plane->frameAttributes().isVisible() ) { 0096 const qreal radius = plane->frameAttributes().cornerRadius(); 0097 QPainterPath path; 0098 path.addRoundedRect( QRectF( plane->translate( QPointF( minValueX, minValueY ) ), 0099 plane->translate( QPointF( maxValueX, maxValueY ) ) ), 0100 radius, radius ); 0101 context->painter()->setClipPath( path ); 0102 } 0103 0104 /* TODO features from old code: 0105 - MAYBE coarsen the main grid when it gets crowded (do it in calculateGrid or here?) 0106 if ( ! dimX.isCalculated ) { 0107 while ( screenRangeX / numberOfUnitLinesX <= MinimumPixelsBetweenLines ) { 0108 dimX.stepWidth *= 10.0; 0109 dimX.subStepWidth *= 10.0; 0110 numberOfUnitLinesX = qAbs( dimX.distance() / dimX.stepWidth ); 0111 } 0112 } 0113 - MAYBE deactivate the sub-grid when it gets crowded 0114 if ( dimX.subStepWidth && (screenRangeX / (dimX.distance() / dimX.subStepWidth) 0115 <= MinimumPixelsBetweenLines) ) { 0116 // de-activating grid sub steps: not enough space 0117 dimX.subStepWidth = 0.0; 0118 } 0119 */ 0120 0121 for ( int i = 0; i < 2; i++ ) { 0122 XySwitch xy( i == 1 ); // first iteration paints the X grid lines, second paints the Y grid lines 0123 const GridAttributes& gridAttrs = xy( gridAttrsX, gridAttrsY ); 0124 bool hasMajorLines = gridAttrs.isGridVisible(); 0125 bool hasMinorLines = hasMajorLines && gridAttrs.isSubGridVisible(); 0126 if ( !hasMajorLines && !hasMinorLines ) { 0127 continue; 0128 } 0129 0130 const DataDimension& dimension = xy( dimX, dimY ); 0131 const bool drawZeroLine = dimension.isCalculated && gridAttrs.zeroLinePen().style() != Qt::NoPen; 0132 0133 QPointF lineStart = QPointF( minValueX, minValueY ); // still need transformation to screen space 0134 QPointF lineEnd = QPointF( maxValueX, maxValueY ); 0135 0136 TickIterator it( xy.isY, dimension, gridAttrs.linesOnAnnotations(), 0137 hasMajorLines, hasMinorLines, plane ); 0138 for ( ; !it.isAtEnd(); ++it ) { 0139 if ( !gridAttrs.isOuterLinesVisible() && 0140 ( it.areAlmostEqual( it.position(), xy( minValueX, minValueY ) ) || 0141 it.areAlmostEqual( it.position(), xy( maxValueX, maxValueY ) ) ) ) { 0142 continue; 0143 } 0144 xy.lvalue( lineStart.rx(), lineStart.ry() ) = it.position(); 0145 xy.lvalue( lineEnd.rx(), lineEnd.ry() ) = it.position(); 0146 QPointF transLineStart = plane->translate( lineStart ); 0147 QPointF transLineEnd = plane->translate( lineEnd ); 0148 if ( ISNAN( transLineStart.x() ) || ISNAN( transLineStart.y() ) || 0149 ISNAN( transLineEnd.x() ) || ISNAN( transLineEnd.y() ) ) { 0150 // ### can we catch NaN problems earlier, wasting fewer cycles? 0151 continue; 0152 } 0153 if ( it.position() == 0.0 && drawZeroLine ) { 0154 p->setPen( PrintingParameters::scalePen( gridAttrsX.zeroLinePen() ) ); 0155 } else if ( it.type() == TickIterator::MinorTick ) { 0156 p->setPen( PrintingParameters::scalePen( gridAttrs.subGridPen() ) ); 0157 } else { 0158 p->setPen( PrintingParameters::scalePen( gridAttrs.gridPen() ) ); 0159 } 0160 p->drawLine( transLineStart, transLineEnd ); 0161 } 0162 } 0163 } 0164 0165 0166 DataDimensionsList CartesianGrid::calculateGrid( const DataDimensionsList& rawDataDimensions ) const 0167 { 0168 Q_ASSERT_X ( rawDataDimensions.count() == 2, "CartesianGrid::calculateGrid", 0169 "Error: calculateGrid() expects a list with exactly two entries." ); 0170 0171 CartesianCoordinatePlane* plane = qobject_cast< CartesianCoordinatePlane* >( mPlane ); 0172 Q_ASSERT_X ( plane, "CartesianGrid::calculateGrid", 0173 "Error: PaintContext::calculatePlane() called, but no cartesian plane set." ); 0174 0175 DataDimensionsList l( rawDataDimensions ); 0176 #if 0 0177 qDebug() << Q_FUNC_INFO << "initial grid X-range:" << l.first().start << "->" << l.first().end 0178 << " substep width:" << l.first().subStepWidth; 0179 qDebug() << Q_FUNC_INFO << "initial grid Y-range:" << l.last().start << "->" << l.last().end 0180 << " substep width:" << l.last().subStepWidth; 0181 #endif 0182 // rule: Returned list is either empty, or it is providing two 0183 // valid dimensions, complete with two non-Zero step widths. 0184 if ( isBoundariesValid( l ) ) { 0185 const QPointF translatedBottomLeft( plane->translateBack( plane->geometry().bottomLeft() ) ); 0186 const QPointF translatedTopRight( plane->translateBack( plane->geometry().topRight() ) ); 0187 0188 const GridAttributes gridAttrsX( plane->gridAttributes( Qt::Horizontal ) ); 0189 const GridAttributes gridAttrsY( plane->gridAttributes( Qt::Vertical ) ); 0190 0191 const DataDimension dimX 0192 = calculateGridXY( l.first(), Qt::Horizontal, 0193 gridAttrsX.adjustLowerBoundToGrid(), 0194 gridAttrsX.adjustUpperBoundToGrid() ); 0195 if ( dimX.stepWidth ) { 0196 //qDebug("CartesianGrid::calculateGrid() l.last().start: %f l.last().end: %f", l.last().start, l.last().end); 0197 //qDebug(" l.first().start: %f l.first().end: %f", l.first().start, l.first().end); 0198 0199 // one time for the min/max value 0200 const DataDimension minMaxY 0201 = calculateGridXY( l.last(), Qt::Vertical, 0202 gridAttrsY.adjustLowerBoundToGrid(), 0203 gridAttrsY.adjustUpperBoundToGrid() ); 0204 0205 if ( plane->autoAdjustGridToZoom() 0206 && plane->axesCalcModeY() == CartesianCoordinatePlane::Linear 0207 && plane->zoomFactorY() > 1.0 ) 0208 { 0209 l.last().start = translatedBottomLeft.y(); 0210 l.last().end = translatedTopRight.y(); 0211 } 0212 // and one other time for the step width 0213 const DataDimension dimY 0214 = calculateGridXY( l.last(), Qt::Vertical, 0215 gridAttrsY.adjustLowerBoundToGrid(), 0216 gridAttrsY.adjustUpperBoundToGrid() ); 0217 if ( dimY.stepWidth ) { 0218 l.first().start = dimX.start; 0219 l.first().end = dimX.end; 0220 l.first().stepWidth = dimX.stepWidth; 0221 l.first().subStepWidth = dimX.subStepWidth; 0222 l.last().start = minMaxY.start; 0223 l.last().end = minMaxY.end; 0224 l.last().stepWidth = dimY.stepWidth; 0225 l.last().subStepWidth = dimY.subStepWidth; 0226 //qDebug() << "CartesianGrid::calculateGrid() final grid y-range:" << l.last().end - l.last().start << " step width:" << l.last().stepWidth << endl; 0227 // calculate some reasonable subSteps if the 0228 // user did not set the sub grid but did set 0229 // the stepWidth. 0230 0231 // FIXME (Johannes) 0232 // the last (y) dimension is not always the dimension for the ordinate! 0233 // since there's no way to check for the orientation of this dimension here, 0234 // we cannot automatically assume substep values 0235 //if ( dimY.subStepWidth == 0 ) 0236 // l.last().subStepWidth = dimY.stepWidth/2; 0237 //else 0238 // l.last().subStepWidth = dimY.subStepWidth; 0239 } 0240 } 0241 } 0242 #if 0 0243 qDebug() << Q_FUNC_INFO << "final grid X-range:" << l.first().start << "->" << l.first().end 0244 << " substep width:" << l.first().subStepWidth; 0245 qDebug() << Q_FUNC_INFO << "final grid Y-range:" << l.last().start << "->" << l.last().end 0246 << " substep width:" << l.last().subStepWidth; 0247 #endif 0248 return l; 0249 } 0250 0251 qreal fastPow10( int x ) 0252 { 0253 qreal res = 1.0; 0254 if ( 0 <= x ) { 0255 for ( int i = 1; i <= x; ++i ) 0256 res *= 10.0; 0257 } else { 0258 for ( int i = -1; i >= x; --i ) 0259 res *= 0.1; 0260 } 0261 return res; 0262 } 0263 0264 #ifdef Q_OS_WIN 0265 #define trunc(x) ((int)(x)) 0266 #endif 0267 0268 DataDimension CartesianGrid::calculateGridXY( 0269 const DataDimension& rawDataDimension, 0270 Qt::Orientation orientation, 0271 bool adjustLower, bool adjustUpper ) const 0272 { 0273 CartesianCoordinatePlane* const plane = dynamic_cast<CartesianCoordinatePlane*>( mPlane ); 0274 if ( ( orientation == Qt::Vertical && plane->autoAdjustVerticalRangeToData() >= 100 ) || 0275 ( orientation == Qt::Horizontal && plane->autoAdjustHorizontalRangeToData() >= 100 ) ) { 0276 adjustLower = false; 0277 adjustUpper = false; 0278 } 0279 0280 DataDimension dim( rawDataDimension ); 0281 if ( dim.isCalculated && dim.start != dim.end ) { 0282 if ( dim.calcMode == AbstractCoordinatePlane::Linear ) { 0283 // linear ( == not-logarithmic) calculation 0284 if ( dim.stepWidth == 0.0 ) { 0285 QList<qreal> granularities; 0286 switch ( dim.sequence ) { 0287 case KChartEnums::GranularitySequence_10_20: 0288 granularities << 1.0 << 2.0; 0289 break; 0290 case KChartEnums::GranularitySequence_10_50: 0291 granularities << 1.0 << 5.0; 0292 break; 0293 case KChartEnums::GranularitySequence_25_50: 0294 granularities << 2.5 << 5.0; 0295 break; 0296 case KChartEnums::GranularitySequence_125_25: 0297 granularities << 1.25 << 2.5; 0298 break; 0299 case KChartEnums::GranularitySequenceIrregular: 0300 granularities << 1.0 << 1.25 << 2.0 << 2.5 << 5.0; 0301 break; 0302 } 0303 //qDebug("CartesianGrid::calculateGridXY() dim.start: %f dim.end: %f", dim.start, dim.end); 0304 calculateStepWidth( 0305 dim.start, dim.end, granularities, orientation, 0306 dim.stepWidth, dim.subStepWidth, 0307 adjustLower, adjustUpper ); 0308 } 0309 // if needed, adjust start/end to match the step width: 0310 //qDebug() << "CartesianGrid::calculateGridXY() has 1st linear range: min " << dim.start << " and max" << dim.end; 0311 0312 AbstractGrid::adjustLowerUpperRange( dim.start, dim.end, dim.stepWidth, 0313 adjustLower, adjustUpper ); 0314 //qDebug() << "CartesianGrid::calculateGridXY() returns linear range: min " << dim.start << " and max" << dim.end; 0315 } else { 0316 // logarithmic calculation with negative values 0317 if ( dim.end <= 0 ) 0318 { 0319 qreal min; 0320 const qreal minRaw = qMin( dim.start, dim.end ); 0321 const int minLog = -static_cast<int>(trunc( log10( -minRaw ) ) ); 0322 if ( minLog >= 0 ) 0323 min = qMin( minRaw, -std::numeric_limits< qreal >::epsilon() ); 0324 else 0325 min = -fastPow10( -(minLog-1) ); 0326 0327 qreal max; 0328 const qreal maxRaw = qMin( -std::numeric_limits< qreal >::epsilon(), qMax( dim.start, dim.end ) ); 0329 const int maxLog = -static_cast<int>(ceil( log10( -maxRaw ) ) ); 0330 if ( maxLog >= 0 ) 0331 max = -1; 0332 else if ( fastPow10( -maxLog ) < maxRaw ) 0333 max = -fastPow10( -(maxLog+1) ); 0334 else 0335 max = -fastPow10( -maxLog ); 0336 if ( adjustLower ) 0337 dim.start = min; 0338 if ( adjustUpper ) 0339 dim.end = max; 0340 dim.stepWidth = -pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) ); 0341 } 0342 // logarithmic calculation (ignoring all negative values) 0343 else 0344 { 0345 qreal min; 0346 const qreal minRaw = qMax( qMin( dim.start, dim.end ), qreal( 0.0 ) ); 0347 const int minLog = static_cast<int>(trunc( log10( minRaw ) ) ); 0348 if ( minLog <= 0 && dim.end < 1.0 ) 0349 min = qMax( minRaw, std::numeric_limits< qreal >::epsilon() ); 0350 else if ( minLog <= 0 ) 0351 min = qMax( qreal(0.00001), dim.start ); 0352 else 0353 min = fastPow10( minLog-1 ); 0354 0355 // Uh oh. Logarithmic scaling doesn't work with a lower or upper 0356 // bound being 0. 0357 const bool zeroBound = dim.start == 0.0 || dim.end == 0.0; 0358 0359 qreal max; 0360 const qreal maxRaw = qMax( qMax( dim.start, dim.end ), qreal( 0.0 ) ); 0361 const int maxLog = static_cast<int>(ceil( log10( maxRaw ) ) ); 0362 if ( maxLog <= 0 ) 0363 max = 1; 0364 else if ( fastPow10( maxLog ) < maxRaw ) 0365 max = fastPow10( maxLog+1 ); 0366 else 0367 max = fastPow10( maxLog ); 0368 if ( adjustLower || zeroBound ) 0369 dim.start = min; 0370 if ( adjustUpper || zeroBound ) 0371 dim.end = max; 0372 dim.stepWidth = pow( 10.0, ceil( log10( qAbs( max - min ) / 10.0 ) ) ); 0373 } 0374 } 0375 } else { 0376 //qDebug() << "CartesianGrid::calculateGridXY() returns stepWidth 1.0 !!"; 0377 // Do not ignore the user configuration 0378 dim.stepWidth = dim.stepWidth ? dim.stepWidth : 1.0; 0379 } 0380 return dim; 0381 } 0382 0383 0384 static void calculateSteps( 0385 qreal start_, qreal end_, const QList<qreal>& list, 0386 int minSteps, int maxSteps, 0387 int power, 0388 qreal& steps, qreal& stepWidth, 0389 bool adjustLower, bool adjustUpper ) 0390 { 0391 //qDebug("-----------------------------------\nstart: %f end: %f power-of-ten: %i", start_, end_, power); 0392 0393 qreal distance = 0.0; 0394 steps = 0.0; 0395 0396 const int lastIdx = list.count()-1; 0397 for ( int i = 0; i <= lastIdx; ++i ) { 0398 const qreal testStepWidth = list.at(lastIdx - i) * fastPow10( power ); 0399 //qDebug( "testing step width: %f", testStepWidth); 0400 qreal start = qMin( start_, end_ ); 0401 qreal end = qMax( start_, end_ ); 0402 //qDebug("pre adjusting start: %f end: %f", start, end); 0403 AbstractGrid::adjustLowerUpperRange( start, end, testStepWidth, adjustLower, adjustUpper ); 0404 //qDebug("post adjusting start: %f end: %f", start, end); 0405 0406 const qreal testDistance = qAbs(end - start); 0407 const qreal testSteps = testDistance / testStepWidth; 0408 0409 //qDebug() << "testDistance:" << testDistance << " distance:" << distance; 0410 if ( (minSteps <= testSteps) && (testSteps <= maxSteps) 0411 && ( (steps == 0.0) || (testDistance <= distance) ) ) { 0412 steps = testSteps; 0413 stepWidth = testStepWidth; 0414 distance = testDistance; 0415 //qDebug( "start: %f end: %f step width: %f steps: %f distance: %f", start, end, stepWidth, steps, distance); 0416 } 0417 } 0418 } 0419 0420 0421 void CartesianGrid::calculateStepWidth( 0422 qreal start_, qreal end_, 0423 const QList<qreal>& granularities, 0424 Qt::Orientation orientation, 0425 qreal& stepWidth, qreal& subStepWidth, 0426 bool adjustLower, bool adjustUpper ) const 0427 { 0428 Q_UNUSED( orientation ); 0429 0430 Q_ASSERT_X ( granularities.count(), "CartesianGrid::calculateStepWidth", 0431 "Error: The list of GranularitySequence values is empty." ); 0432 QList<qreal> list( granularities ); 0433 std::sort(list.begin(), list.end()); 0434 0435 const qreal start = qMin( start_, end_); 0436 const qreal end = qMax( start_, end_); 0437 const qreal distance = end - start; 0438 //qDebug( "raw data start: %f end: %f", start, end); 0439 0440 qreal steps; 0441 int power = 0; 0442 while ( list.last() * fastPow10( power ) < distance ) { 0443 ++power; 0444 }; 0445 // We have the sequence *two* times in the calculation test list, 0446 // so we will be sure to find the best match: 0447 const int count = list.count(); 0448 QList<qreal> testList; 0449 0450 for ( int dec = -1; dec == -1 || fastPow10( dec + 1 ) >= distance; --dec ) 0451 for ( int i = 0; i < count; ++i ) 0452 testList << list.at(i) * fastPow10( dec ); 0453 0454 testList << list; 0455 0456 do { 0457 calculateSteps( start, end, testList, m_minsteps, m_maxsteps, power, 0458 steps, stepWidth, 0459 adjustLower, adjustUpper ); 0460 --power; 0461 } while ( steps == 0.0 ); 0462 ++power; 0463 //qDebug( "steps calculated: stepWidth: %f steps: %f", stepWidth, steps); 0464 0465 // find the matching sub-grid line width in case it is 0466 // not set by the user 0467 0468 if ( subStepWidth == 0.0 ) { 0469 if ( stepWidth == list.first() * fastPow10( power ) ) { 0470 subStepWidth = list.last() * fastPow10( power-1 ); 0471 //qDebug("A"); 0472 } else if ( stepWidth == list.first() * fastPow10( power-1 ) ) { 0473 subStepWidth = list.last() * fastPow10( power-2 ); 0474 //qDebug("B"); 0475 } else { 0476 qreal smallerStepWidth = list.first(); 0477 for ( int i = 1; i < list.count(); ++i ) { 0478 if ( stepWidth == list.at( i ) * fastPow10( power ) ) { 0479 subStepWidth = smallerStepWidth * fastPow10( power ); 0480 break; 0481 } 0482 if ( stepWidth == list.at( i ) * fastPow10( power-1 ) ) { 0483 subStepWidth = smallerStepWidth * fastPow10( power-1 ); 0484 break; 0485 } 0486 smallerStepWidth = list.at( i ); 0487 } 0488 } 0489 } 0490 //qDebug("CartesianGrid::calculateStepWidth() found stepWidth %f (%f steps) and sub-stepWidth %f", stepWidth, steps, subStepWidth); 0491 }